text: #include "Hosting.h"
  
  using namespace std;
  
  #if defined(_WIN32)
  	#if !defined(BITS)
  		#define BITS 64
  	#endif
  	#if (BITS == 64)
  		#define SDK_PATH "./parsec.dll"
  	#else
  		#define SDK_PATH "./parsec32.dll"
  	#endif
  #endif
  
  // ============================================================
  // 
  //  PUBLIC
  // 
  // ============================================================
  
  Hosting::Hosting()
  {
  	_hostConfig = EMPTY_HOST_CONFIG;
  	MetadataCache::loadPreferences();
  	setHostConfig(
  		MetadataCache::preferences.roomName,
  		MetadataCache::preferences.gameID,
  		MetadataCache::preferences.guestCount,
  		MetadataCache::preferences.publicRoom,
  		MetadataCache::preferences.secret
  	);
  	setHostVideoConfig(MetadataCache::preferences.fps, MetadataCache::preferences.bandwidth);
  
  	_sfxList.init("./sfx/custom/_sfx.json");
  	
  	_tierList.loadTiers();
  	_tierList.saveTiers();
  
  	vector<GuestData> banned = MetadataCache::loadBannedUsers();
  	_banList = BanList(banned);
  
  	_parsec = nullptr;
  
  	SDL_Init(SDL_INIT_JOYSTICK);
  	_masterOfPuppets.init(_gamepadClient);
  	_masterOfPuppets.start();
  }
  
  void Hosting::applyHostConfig()
  {
  	if (isRunning())
  	{
  		ParsecHostSetConfig(_parsec, &_hostConfig, _parsecSession.sessionId.c_str());
  	}
  }
  
  void Hosting::broadcastChatMessage(string message)
  {
  	vector<Guest> guests = _guestList.getGuests();
  	vector<Guest>::iterator gi;
  
  	for (gi = guests.begin(); gi != guests.end(); ++gi)
  	{
  		ParsecHostSendUserData(_parsec, (*gi).id, HOSTING_CHAT_MSG_ID, message.c_str());
  	}
  }
  
  void Hosting::init()
  {
  	_parsecStatus = ParsecInit(NULL, NULL, (char *)SDK_PATH, &_parsec);
  	_dx11.init();
  	_gamepadClient.setParsec(_parsec);
  	_gamepadClient.init();
  
  	MetadataCache::Preferences preferences = MetadataCache::loadPreferences();
  
  	_createGamepadsThread = thread([&]() {
  		_gamepadClient.createAllGamepads();
  		_createGamepadsThread.detach();
  	});
  
  	audioOut.fetchDevices();
  	vector<AudioOutDevice> audioOutDevices = audioOut.getDevices();
  	if (preferences.audioOutputDevice >= audioOutDevices.size()) {
  		preferences.audioOutputDevice = 0;
  	}
  	audioOut.setOutputDevice(preferences.audioOutputDevice);		// TODO Fix leak in setOutputDevice
  	audioOut.captureAudio();
  	audioOut.volume = 0.3f;
  	audioOut.setFrequency((Frequency)MetadataCache::preferences.speakersFrequency);
  
  	vector<AudioInDevice> audioInputDevices = audioIn.listInputDevices();
  	if (preferences.audioInputDevice >= audioInputDevices.size()) {
  		preferences.audioInputDevice = 0;
  	}
  	AudioInDevice device = audioIn.selectInputDevice(preferences.audioInputDevice);
  	audioIn.init(device);
  	audioIn.volume = 0.8f;
  	audioIn.setFrequency((Frequency)MetadataCache::preferences.micFrequency);
  
  	preferences.isValid = true;
  	MetadataCache::savePreferences(preferences);
  	_parsecSession.loadThumbnails();
  	_parsecSession.loadSessionCache();
  
  	fetchAccountData();
  
  	_chatBot = new ChatBot(
  		audioIn, audioOut, _banList, _dx11,
  		_gamepadClient, _guestList, _guestHistory, _parsec,
  		_hostConfig, _parsecSession, _sfxList, _tierList,
  		_isRunning, _host
  	);
  
  	CommandBonk::init();
  }
  
  void Hosting::release()
  {
  	stopHosting();
  	while (_isRunning)
  	{
  		Sleep(5);
  	}
  	_dx11.clear();
  	_gamepadClient.release();
  	_masterOfPuppets.stop();
  }
  
  bool Hosting::isReady()
  {
  	return _parsecStatus == PARSEC_OK;
  }
  
  bool Hosting::isRunning()
  {
  	return _isRunning;
  }
  
  bool& Hosting::isGamepadLock()
  {
  	return _gamepadClient.lock;
  }
  
  Guest& Hosting::getHost()
  {
  	return _host;
  }
  
  ParsecSession& Hosting::getSession()
  {
  	return _parsecSession;
  }
  
  void Hosting::fetchAccountData(bool sync)
  {
  	_host.name = "Host";
  	_host.status = Guest::Status::INVALID;
  	if (isReady())
  	{
  		if (sync)
  		{
  			_parsecSession.fetchAccountDataSync(&_host);
  		}
  		else
  		{
  			_parsecSession.fetchAccountData(&_host);
  		}
  	}
  }
  
  ParsecHostConfig& Hosting::getHostConfig()
  {
  	return _hostConfig;
  }
  
  DX11& Hosting::getDX11()
  {
  	return _dx11;
  }
  
  ChatBot* Hosting::getChatBot()
  {
  	return _chatBot;
  }
  
  vector<string>& Hosting::getMessageLog()
  {
  	return _chatLog.getMessageLog();
  }
  
  vector<string>& Hosting::getCommandLog()
  {
  	return _chatLog.getCommandLog();
  }
  
  vector<Guest>& Hosting::getGuestList()
  {
  	return _guestList.getGuests();
  }
  
  vector<GuestData>& Hosting::getGuestHistory()
  {
  	return _guestHistory.getGuests();
  }
  
  BanList& Hosting::getBanList()
  {
  	return _banList;
  }
  
  vector<AGamepad*>& Hosting::getGamepads()
  {
  	return _gamepadClient.gamepads;
  }
  
  GamepadClient& Hosting::getGamepadClient()
  {
  	return _gamepadClient;
  }
  
  MasterOfPuppets& Hosting::getMasterOfPuppets()
  {
  	return _masterOfPuppets;
  }
  
  const char** Hosting::getGuestNames()
  {
  	return _guestList.guestNames;
  }
  
  void Hosting::toggleGamepadLock()
  {
  	_gamepadClient.toggleLock();
  }
  
  void Hosting::setGameID(string gameID)
  {
  	try
  	{
  		strcpy_s(_hostConfig.gameID, gameID.c_str());
  	}
  	catch (const std::exception&) {}
  }
  
  void Hosting::setMaxGuests(uint8_t maxGuests)
  {
  	_hostConfig.maxGuests = maxGuests;
  }
  
  void Hosting::setHostConfig(string roomName, string gameId, uint8_t maxGuests, bool isPublicRoom)
  {
  	setRoomName(roomName);
  	setGameID(gameId);
  	setMaxGuests(maxGuests);
  	setPublicRoom(isPublicRoom);
  }
  
  void Hosting::setHostConfig(string roomName, string gameId, uint8_t maxGuests, bool isPublicRoom, string secret)
  {
  	setRoomName(roomName);
  	setGameID(gameId);
  	setMaxGuests(maxGuests);
  	setPublicRoom(isPublicRoom);
  	setRoomSecret(secret);
  }
  
  void Hosting::setHostVideoConfig(uint32_t fps, uint32_t bandwidth)
  {
  	_hostConfig.video->encoderFPS = fps;
  	_hostConfig.video->encoderMaxBitrate = bandwidth;
  	MetadataCache::preferences.fps = fps;
  	MetadataCache::preferences.bandwidth = bandwidth;
  }
  
  void Hosting::setPublicRoom(bool isPublicRoom)
  {
  	_hostConfig.publicGame = isPublicRoom;
  }
  
  void Hosting::setRoomName(string roomName)
  {
  	try
  	{
  		strcpy_s(_hostConfig.name, roomName.c_str());
  	}
  	catch (const std::exception&) {}
  }
  
  void Hosting::setRoomSecret(string secret)
  {
  	try
  	{
  		strcpy_s(_hostConfig.secret, secret.c_str());
  	}
  	catch (const std::exception&) {}
  }
  
  void Hosting::startHosting()
  {
  	if (!_isRunning)
  	{
  		_isRunning = true;
  		initAllModules();
  
  		try
  		{
  			if (_parsec != nullptr)
  			{
  				_mediaThread = thread ( [this]() {liveStreamMedia(); } );
  				_inputThread = thread ([this]() {pollInputs(); });
  				_eventThread = thread ([this]() {pollEvents(); });
  				_mainLoopControlThread = thread ([this]() {mainLoopControl(); });
  			}
  		}
  		catch (const exception&)
  		{}
  	}
  
  	bool debug = true;
  }
  
  void Hosting::stopHosting()
  {
  	_isRunning = false;
  	_guestList.clear();
  }
  
  void Hosting::stripGamepad(int index)
  {
  	_gamepadClient.clearOwner(index);
  }
  
  void Hosting::setOwner(AGamepad& gamepad, Guest newOwner, int padId)
  {
  	bool found = _gamepadClient.findPreferences(newOwner.userID, [&](GamepadClient::GuestPreferences& prefs) {
  		gamepad.setOwner(newOwner, padId, prefs.mirror);
  	});
  
  	if (!found)
  	{
  		gamepad.setOwner(newOwner, padId, false);
  	}
  }
  
  void Hosting::handleMessage(const char* message, Guest& guest, bool isHost, bool isHidden)
  {
  	ACommand* command = _chatBot->identifyUserDataMessage(message, guest, isHost);
  	command->run();
  
  	// Non-blocked default message
  	if (!isFilteredCommand(command))
  	{
  		Tier tier = _tierList.getTier(guest.userID);
  
  		CommandDefaultMessage defaultMessage(message, guest, _chatBot->getLastUserId(), tier);
  		defaultMessage.run();
  		_chatBot->setLastUserId(guest.userID);
  
  		if (!defaultMessage.replyMessage().empty() && !isHidden)
  		{
  			broadcastChatMessage(defaultMessage.replyMessage());
  			
  			string adjustedMessage = defaultMessage.replyMessage();
  			Stringer::replacePatternOnce(adjustedMessage, "%", "%%");
  			_chatLog.logMessage(adjustedMessage);
  		}
  	}
  
  	// Chatbot's command reply
  	if (!command->replyMessage().empty() && command->type() != COMMAND_TYPE::DEFAULT_MESSAGE)
  	{
  		broadcastChatMessage(command->replyMessage());
  		_chatLog.logCommand(command->replyMessage());
  		_chatBot->setLastUserId();
  	}
  
  	delete command;
  }
  
  void Hosting::sendHostMessage(const char* message, bool isHidden)
  {
  	static bool isAdmin = true;
  	handleMessage(message, _host, true, isHidden);
  }
  
  
  // ============================================================
  // 
  //  PRIVATE
  // 
  // ============================================================
  void Hosting::initAllModules()
  {
  	// Instance all gamepads at once
  	_connectGamepadsThread = thread([&]() {
  		_gamepadClient.sortGamepads();
  		_connectGamepadsThread.detach();
  	});
  
  	parsecArcadeStart();
  }
  
  void Hosting::liveStreamMedia()
  {
  	_mediaMutex.lock();
  	_isMediaThreadRunning = true;
  
  	static uint32_t sleepTimeMs = 4;
  	_mediaClock.setDuration(sleepTimeMs);
  	_mediaClock.start();
  
  	while (_isRunning)
  	{
  		_mediaClock.reset();
  
  		_dx11.captureScreen(_parsec);
  
  		if (audioIn.isEnabled && audioOut.isEnabled)
  		{
  			audioIn.captureAudio();
  			audioOut.captureAudio();
  			if (audioIn.isReady() && audioOut.isReady())
  			{
  				vector<int16_t> mixBuffer = _audioMix.mix(audioIn.popBuffer(), audioOut.popBuffer());
  				ParsecHostSubmitAudio(_parsec, PCM_FORMAT_INT16, audioOut.getFrequencyHz(), mixBuffer.data(), (uint32_t)mixBuffer.size() / 2);
  			}
  		}
  		else if (audioOut.isEnabled)
  		{
  			audioOut.captureAudio();
  			if (audioOut.isReady())
  			{
  				vector<int16_t> buffer = audioOut.popBuffer();
  				ParsecHostSubmitAudio(_parsec, PCM_FORMAT_INT16, audioOut.getFrequencyHz(), buffer.data(), (uint32_t)buffer.size() / 2);
  			}
  		}
  		else
  		{
  			audioIn.captureAudio();
  			if (audioIn.isReady())
  			{
  				vector<int16_t> buffer = audioIn.popBuffer();
  				ParsecHostSubmitAudio(_parsec, PCM_FORMAT_INT16, (uint32_t)audioIn.getFrequency(), buffer.data(), (uint32_t)buffer.size() / 2);
  			}
  		}
  
  		sleepTimeMs = _mediaClock.getRemainingTime();
  		if (sleepTimeMs > 0)
  		{
  			Sleep(sleepTimeMs);
  		}
  	}
  
  	_isMediaThreadRunning = false;
  	_mediaMutex.unlock();
  	_mediaThread.detach();
  }
  
  void Hosting::mainLoopControl()
  {
  	do
  	{
  		Sleep(50);
  	} while (!_isRunning);
  
  	_isRunning = true;
  
  	_mediaMutex.lock();
  	_inputMutex.lock();
  	_eventMutex.lock();
  
  	ParsecHostStop(_parsec);
  	_isRunning = false;
  
  	_mediaMutex.unlock();
  	_inputMutex.unlock();
  	_eventMutex.unlock();
  
  	_mainLoopControlThread.detach();
  }
  
  void Hosting::pollEvents()
  {
  	_eventMutex.lock();
  	_isEventThreadRunning = true;
  
  	string chatBotReply;
  
  	ParsecGuest* guests = nullptr;
  	int guestCount = 0;
  
  	ParsecHostEvent event;
  
  	while (_isRunning)
  	{
  		if (ParsecHostPollEvents(_parsec, 30, &event)) {
  			ParsecGuest parsecGuest = event.guestStateChange.guest;
  			ParsecGuestState state = parsecGuest.state;
  			Guest guest = Guest(parsecGuest.name, parsecGuest.userID, parsecGuest.id);
  			guestCount = ParsecHostGetGuests(_parsec, GUEST_CONNECTED, &guests);
  			_guestList.setGuests(guests, guestCount);
  
  			switch (event.type)
  			{
  			case HOST_EVENT_GUEST_STATE_CHANGE:
  				onGuestStateChange(state, guest);
  				break;
  
  			case HOST_EVENT_USER_DATA:
  				char* msg = (char*)ParsecGetBuffer(_parsec, event.userData.key);
  
  				if (event.userData.id == PARSEC_APP_CHAT_MSG)
  				{
  					handleMessage(msg, guest);
  				}
  
  				ParsecFree(_parsec, msg);
  				break;
  			}
  		}
  	}
  
  	ParsecFree(_parsec, guests);
  	_isEventThreadRunning = false;
  	_eventMutex.unlock();
  	_eventThread.detach();
  }
  
  void Hosting::pollInputs()
  {
  	_inputMutex.lock();
  	_isInputThreadRunning = true;
  
  	ParsecGuest inputGuest;
  	ParsecMessage inputGuestMsg;
  
  	while (_isRunning)
  	{
  		if (ParsecHostPollInput(_parsec, 4, &inputGuest, &inputGuestMsg))
  		{
  			if (!_gamepadClient.lock)
  			{
  				_gamepadClient.sendMessage(inputGuest, inputGuestMsg);
  			}
  		}
  	}
  
  	_isInputThreadRunning = false;
  	_inputMutex.unlock();
  	_inputThread.detach();
  }
  
  bool Hosting::parsecArcadeStart()
  {
  	if (isReady()) {
  		//ParsecSetLogCallback(logCallback, NULL);
  		ParsecStatus status = ParsecHostStart(_parsec, HOST_GAME, &_hostConfig, _parsecSession.sessionId.c_str());
  		return status == PARSEC_OK;
  	}
  	return false;
  }
  
  bool Hosting::isFilteredCommand(ACommand* command)
  {
  	static vector<COMMAND_TYPE> filteredCommands{ COMMAND_TYPE::IP };
  
  	COMMAND_TYPE type;
  	for (vector<COMMAND_TYPE>::iterator it = filteredCommands.begin(); it != filteredCommands.end(); ++it)
  	{
  		type = command->type();
  		if (command->type() == *it)
  		{
  			return true;
  		}
  	}
  
  	return false;
  }
  
  void Hosting::onGuestStateChange(ParsecGuestState& state, Guest& guest)
  {
  	static string logMessage;
  
  	if ((state == GUEST_CONNECTED || state == GUEST_CONNECTING) && _banList.isBanned(guest.userID))
  	{
  		ParsecHostKickGuest(_parsec, guest.id);
  		logMessage = _chatBot->formatBannedGuestMessage(guest);
  		broadcastChatMessage(logMessage);
  	}
  	else if (state == GUEST_CONNECTED || state == GUEST_DISCONNECTED)
  	{
  		static string guestMsg;
  		guestMsg.clear();
  		guestMsg = string(guest.name);
  
  		logMessage = _chatBot->formatGuestConnection(guest, state);
  		broadcastChatMessage(logMessage);
  		_chatLog.logCommand(logMessage);
  
  		if (state == GUEST_CONNECTED)
  		{
  			_guestHistory.add(GuestData(guest.name, guest.userID));
  		}
  		else
  		{
  			int droppedPads = 0;
  			CommandFF command(guest, _gamepadClient);
  			command.run();
  			if (droppedPads > 0)
  			{
  				logMessage = command.replyMessage();
  				broadcastChatMessage(logMessage);
  				_chatLog.logCommand(logMessage);
  			}
  		}
  	}
  }