#include "cmclient.h"

#include "../chat.h"

#include <engine/shared/config.h>

#include <generated/protocol.h>

#include <game/client/gameclient.h>

CCMClient::CCMClient()
{
	OnReset();
}

void CCMClient::ConAutoTeam(IConsole::IResult *pResult, void *pUserData)
{
	char aBuf[256];
	if(((CCMClient *)pUserData)->m_TeamNum < 0)
	{
		if(g_Config.m_ClAutoTeamLog)
			((CCMClient *)pUserData)->GameClient()->m_Chat.Echo("No one has joined a team yet");
		return;
	}
	if(g_Config.m_ClAutoTeamLog)
	{
		str_format(aBuf, sizeof(aBuf), "Following '%s' to team %i", ((CCMClient *)pUserData)->m_TeamFollowed, ((CCMClient *)pUserData)->m_TeamNum);
		((CCMClient *)pUserData)->GameClient()->m_Chat.Echo(aBuf);
	}
	str_format(aBuf, sizeof(aBuf), "/team %i", ((CCMClient *)pUserData)->m_TeamNum);
	((CCMClient *)pUserData)->GameClient()->m_Chat.SendChat(0, aBuf);
}

void CCMClient::ConAutoSwap(IConsole::IResult *pResult, void *pUserData)
{
	CCMClient *pSelf = (CCMClient *)pUserData;
	if(pSelf->m_AutoSwapState != AUTOSWAP_IDLE)
	{
		pSelf->GameClient()->m_Chat.Echo("Canceling an active auto swap");
		pSelf->m_AutoSwapState = AUTOSWAP_IDLE;
		pSelf->m_AutoSwapTarget = -1;
	}
	char aBuf[256];
	const char *pName = pResult->GetString(0);
	if(!pName || pName[0] == '\0')
	{
		if(pSelf->m_AutoSwapTargetGuess < 0 || g_Config.m_ClDummy != pSelf->m_AutoSwapDummyGuess)
		{
			pSelf->GameClient()->m_Chat.Echo("No target provided and no recent swap requests");
			return;
		}
		pSelf->m_AutoSwapTarget = pSelf->m_AutoSwapTargetGuess;
		pName = pSelf->GameClient()->m_aClients[pSelf->m_AutoSwapTargetGuess].m_aName;
		str_format(aBuf, sizeof(aBuf), "No target provided, assuming %s", pName);
		pSelf->GameClient()->m_Chat.Echo(aBuf);
	}
	else
	{
		pSelf->m_AutoSwapTarget = -1;
		for(int i = 0; i < MAX_CLIENTS; i++)
		{
			CGameClient::CClientData *client = &pSelf->GameClient()->m_aClients[i];
			if(!client->m_Active)
				continue;
			if(!strcmp(client->m_aName, pName))
			{
				pSelf->m_AutoSwapTarget = i;
				break;
			}
		}
		if(pSelf->m_AutoSwapTarget < 0)
		{
			pSelf->GameClient()->m_Chat.Echo("No player found with that name");
			return;
		}
	}
	str_format(aBuf, sizeof(aBuf), "/swap %s", pName);
	pSelf->GameClient()->m_Chat.SendChat(0, aBuf);
	pSelf->m_AutoSwapState = AUTOSWAP_INIT;
	pSelf->m_AutoSwapDummy = g_Config.m_ClDummy;
	pSelf->m_AutoSwapTargetGuess = -1;
}

void CCMClient::ConDumpSkin(IConsole::IResult *pResult, void *pUserData)
{
	CCMClient *pSelf = (CCMClient *)pUserData;
	const char *pName = pResult->GetString(0);
	if(!pName || pName[0] == '\0')
	{
		pSelf->GameClient()->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "No target given");
		return;
	}
	for(int i = 0; i < MAX_CLIENTS; i++)
	{
		CGameClient::CClientData *client = &pSelf->GameClient()->m_aClients[i];
		if(!strcmp(pName, client->m_aName))
		{
			char aBuf[256];
			if(client->m_UseCustomColor)
				str_format(aBuf, sizeof(aBuf), "%s is using skin %s with colors %i, %i (Hue: %03i Sat.: %03i Lht.: %03i, Hue: %03i Sat.: %03i Lht.: %03i)", pName, client->m_aSkinName, client->m_ColorBody, client->m_ColorFeet, (client->m_ColorBody >> 16) & 0xff, (client->m_ColorBody >> 8) & 0xff, (client->m_ColorBody >> 0) & 0xff, (client->m_ColorFeet >> 16) & 0xff, (client->m_ColorFeet >> 8) & 0xff, (client->m_ColorFeet >> 0) & 0xff);
			else
				str_format(aBuf, sizeof(aBuf), "%s is using skin %s", pName, client->m_aSkinName);
			pSelf->GameClient()->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", aBuf);
			return;
		}
	}
	pSelf->GameClient()->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "No player found with that name");
}

void CCMClient::OnInit()
{
	m_pGraphics = Kernel() ? Kernel()->RequestInterface<IEngineGraphics>() : nullptr;
}

void CCMClient::OnReset()
{
	m_TeamFollowed[0] = 0;
	m_TeamNum = -1;
	m_AutoSwapState = AUTOSWAP_IDLE;
	m_AutoSwapTarget = -1, m_AutoSwapTargetGuess = -1;
	for(int i = 0; i < NUM_DUMMIES; i++)
	{
		m_IdleMoveNotifiedEnd[i] = -1, m_IdlePositionStableStart[i] = -1;
		m_IdlePosition[i] = vec2(0, 0);
	}
	m_IsInactive = false;
	m_InactiveTimeout = SERVER_TICK_SPEED;
}

void CCMClient::OnUpdate()
{
	switch(m_AutoSwapState)
	{
	case AUTOSWAP_IDLE:
		break;
	case AUTOSWAP_INIT:
		break;
	case AUTOSWAP_DELAY:
		if(time() > m_AutoSwapDelayExpire)
		{
			m_AutoSwapState = AUTOSWAP_UNSAFE;
			m_AutoSwapSafeStart = time();
			m_AutoSwapDelayExpire = time() + (10 * time_freq());
			if(g_Config.m_ClDummy != m_AutoSwapDummy)
				GameClient()->m_Chat.Echo("Auto swap is ready, but you are currently not controlling the swapping player");
		}
		break;
	case AUTOSWAP_UNSAFE:
	{
		if(!GameClient()->m_aClients[m_AutoSwapTarget].m_Active)
		{
			GameClient()->m_Chat.Echo("Auto swap canceled because the target is no longer active");
			m_AutoSwapState = AUTOSWAP_IDLE;
			m_AutoSwapTarget = -1;
		}
		int states[2] = {0, 0};
		int ids[2] = {GameClient()->m_aLocalIds[0], m_AutoSwapTarget};
		for(int i = 0; i < 2; i++)
		{
			CGameClient::CClientData *client = &GameClient()->m_aClients[ids[i]];
			//if (client->m_Predicted.m_FreezeEnd == 0)
			//{
			bool Grounded = false;
			if(Collision()->CheckPoint(client->m_RenderCur.m_X + CCharacterCore::PhysicalSize() / 2,
				   client->m_RenderCur.m_Y + CCharacterCore::PhysicalSize() / 2 + 5))
			{
				Grounded = true;
			}
			if(Collision()->CheckPoint(client->m_RenderCur.m_X - CCharacterCore::PhysicalSize() / 2,
				   client->m_RenderCur.m_Y + CCharacterCore::PhysicalSize() / 2 + 5))
			{
				Grounded = true;
			}
			if(!Grounded)
				states[i] = 4;
			if(client->m_Predicted.m_Vel.x < (-7.81f * 2.0f) || client->m_Predicted.m_Vel.x > (7.81f * 2.0f) || client->m_Predicted.m_Vel.y < (-7.81f * 2.0f) || client->m_Predicted.m_Vel.y > (7.81f * 2.0f))
				states[i] = 3;
			//}
			if(client->m_Predicted.m_HookState > 0) // TODO: what values are 'hooked to surface' and 'hooked to player'
				states[i] = 2;
			if(client->m_Predicted.m_Direction || (client->m_Predicted.m_Jumped & 1))
				states[i] = 1;
		}
		static const char *msgs[] = {nullptr, "are not idle", "are hooking something", "are moving too fast", "are not grounded"};
		if((states[0] != 0 && states[0] != 4) || (states[1] != 0 && states[1] != 4))
			m_AutoSwapSafeStart = time();
		if(g_Config.m_ClDummy == m_AutoSwapDummy && time() > m_AutoSwapSafeStart + (((states[0] != 0 || states[1] != 0) ? 5 : 1) * time_freq()))
		{
			char aBuf[256];
			str_format(aBuf, sizeof(aBuf), "/swap %s", GameClient()->m_aClients[m_AutoSwapTarget].m_aName);
			GameClient()->m_Chat.SendChat(0, aBuf);
			m_AutoSwapState = AUTOSWAP_IDLE;
		}
		else if(m_AutoSwapDelayExpire != 0 && time() > m_AutoSwapDelayExpire && ((states[0] != 0 && states[0] != 4) || (states[1] != 0 && states[1] != 4)))
		{
			char aBuf[256];
			if(states[0] != 0 && states[1] != 0)
				str_format(aBuf, sizeof(aBuf), "Auto swap has been ready for 10 seconds, but swapping isn't safe yet (%syou %s, they %s)", g_Config.m_ClDummy == m_AutoSwapDummy ? "" : "you are not controlling the swapping player, ", msgs[states[0]], msgs[states[1]]);
			else
				str_format(aBuf, sizeof(aBuf), "Auto swap has been ready for 10 seconds, but swapping isn't safe yet (%s%s %s)", g_Config.m_ClDummy == m_AutoSwapDummy ? "" : "you are not controlling the swapping player, ", states[0] == 0 ? "they" : "you", msgs[states[0] == 0 ? states[1] : states[0]]);
			GameClient()->m_Chat.Echo(aBuf);
			m_AutoSwapDelayExpire = 0;
		}
		break;
	}
	}
}

void CCMClient::OnNewSnapshot()
{
	for(int i = 0; i < NUM_DUMMIES; i++)
	{
		if(GameClient()->m_aLocalIds[i] < 0)
			continue;
		CGameClient::CClientData *pClient = &GameClient()->m_aClients[GameClient()->m_aLocalIds[i]];
		if(!pClient->m_Paused || g_Config.m_ClPausedMoveNotifyTimeout == 0)
		{
			m_IdleMoveNotifiedEnd[i] = -1;
			m_IdlePosition[i] = vec2(-7000, -7000);
			m_IdlePositionStableStart[i] = -1;
			continue;
		}
		if(m_IdleMoveNotifiedEnd[i] >= Client()->GameTick(i) && m_IdlePositionStableStart[i] >= 0)
			continue;
		CGameClient::CSnapState::CCharacterInfo *pCharacter = &GameClient()->m_Snap.m_aCharacters[GameClient()->m_aLocalIds[i]];
		vec2 Pos = vec2(pCharacter->m_Cur.m_X, pCharacter->m_Cur.m_Y);
		if(m_IdlePositionStableStart[i] < 0)
		{
			m_IdlePosition[i] = Pos;
			m_IdlePositionStableStart[i] = Client()->GameTick(i);
		}
		else if(Client()->GameTick(i) >= m_IdlePositionStableStart[i] + (1 * Client()->GameTickSpeed()))
		{
			if(Pos != m_IdlePosition[i] && (Client()->GameTick(i) >= m_IdleMoveNotifiedEnd[i]))
			{
				GameClient()->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CTF_GRAB_PL, 1.0f);
				m_IdleMoveNotifiedEnd[i] = Client()->GameTick(i) + (g_Config.m_ClPausedMoveNotifyTimeout * Client()->GameTickSpeed());
				m_IdlePositionStableStart[i] = Client()->GameTick(i);
			}
		}
		else if(Pos != m_IdlePosition[i])
		{
			m_IdlePosition[i] = Pos;
			m_IdlePositionStableStart[i] = Client()->GameTick(i);
		}
	}

	bool IsInactive = Client()->State() != IClient::STATE_DEMOPLAYBACK && (((g_Config.m_ClInactiveConditions & 4) && !m_pGraphics->WindowActive()) || ((g_Config.m_ClInactiveConditions & 1) && GameClient()->m_Menus.IsActive()) || ((g_Config.m_ClInactiveConditions & 2) && g_Config.m_ClEditor));
	if(m_InactiveTimeout > 0)
		m_InactiveTimeout--;
	else if(IsInactive)
	{
		if(!m_IsInactive)
		{
			for(int i = 0; i < NUM_DUMMIES; i++)
			{
				m_EmoteStop[i] = -1;
				if(g_Config.m_ClInactivePause && GameClient()->m_aLocalIds[i] >= 0 && !GameClient()->m_aClients[GameClient()->m_aLocalIds[i]].m_Paused && !GameClient()->m_aClients[GameClient()->m_aLocalIds[i]].m_Spec)
				{
					GameClient()->m_Chat.SendChatConn(i, 0, "/pause");
					m_WasPaused[i] = false;
				}
				else
					m_WasPaused[i] = true;
			}
		}
		for(int i = 0; i < NUM_DUMMIES; i++)
		{
			if(GameClient()->m_aLocalIds[i] < 0)
				continue;
			CGameClient::CClientData *pClient = &GameClient()->m_aClients[GameClient()->m_aLocalIds[i]];
			if(!pClient->m_Active || pClient->m_Afk || pClient->m_Spec || pClient->m_Solo)
				continue;
			if(g_Config.m_ClInactiveZzz)
			{
				bool WantEmote = false;
				if(!pClient->m_DeepFrozen)
				{
					// frozen value covers the default player show distance
					// unfrozen value is four tiles
					const float DistanceThreshold = pClient->m_FreezeEnd > 0 ? 1440.0f : 4.0f * 32.0f;
					for(int j = 0; j < MAX_CLIENTS; j++)
					{
						CGameClient::CClientData *pOther = &GameClient()->m_aClients[j];
						bool IsSelf = false;
						for(int k = 0; k < NUM_DUMMIES; k++)
						{
							if(j == GameClient()->m_aLocalIds[k])
							{
								IsSelf = true;
								break;
							}
						}
						if(IsSelf || !pOther->m_Active || pOther->m_Afk || pOther->m_Paused || pOther->m_Spec || pOther->m_Solo || pOther->m_FreezeEnd != 0)
							continue;
						float Distance = distance(pClient->m_RenderPos, pOther->m_RenderPos);
						if(Distance < DistanceThreshold)
						{
							WantEmote = true;
							break;
						}
					}
				}
				if(WantEmote && Client()->GameTick(i) >= m_EmoteStop[i])
				{
					CNetMsg_Cl_Emoticon Msg;
					Msg.m_Emoticon = EMOTICON_ZZZ;
					Client()->SendPackMsg(i, &Msg, MSGFLAG_VITAL);
					m_EmoteStop[i] = Client()->GameTick(i) + 2 * Client()->GameTickSpeed();
				}
			}
		}
		m_IsInactive = true;
	}
	else
	{
		if(m_IsInactive)
		{
			for(int i = 0; i < NUM_DUMMIES; i++)
			{
				if(!m_WasPaused[i])
					GameClient()->m_Chat.SendChatConn(i, 0, "/pause");
			}
		}
		m_IsInactive = false;
	}
}

void CCMClient::OnMessage(int MsgType, void *pRawMsg)
{
	if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
		return;

	if(MsgType == NETMSGTYPE_SV_CHAT)
	{
		CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
		int ClientId = pMsg->m_ClientId;

		const char *pLine = pMsg->m_pMessage;
		if(ClientId == -1) // should be CChat::SERVER_MSG...
		{
			// auto team tracking
			if(str_find(pLine, "' joined team ") && pLine[0] == '\'')
			{
				int nameEnd = 0;
				for(int i = 0; i < 127; i++)
				{
					if(strncmp(&pLine[1 + i], "' joined team ", 14) == 0)
					{
						nameEnd = i;
						break;
					}
				}
				if(nameEnd != 0)
				{
					int numStart = 1 + nameEnd + 14;
					int teamNum = pLine[numStart + 0] - '0';
					if(teamNum >= 0 && teamNum <= 9)
					{
						int teamNum2 = pLine[numStart + 1] - '0';
						if(teamNum2 >= 0 && teamNum2 <= 9)
							teamNum = teamNum * 10 + teamNum2;
						m_TeamNum = teamNum;
						memcpy(m_TeamFollowed, &pLine[1], nameEnd);
						m_TeamFollowed[nameEnd] = 0;
					}
				}
			}
			// auto swap auto target
			if(m_AutoSwapState == AUTOSWAP_IDLE && str_find(pLine, " has requested to swap with you. To complete the swap process please wait "))
			{
				int nameEnd = 0;
				for(int i = 0; i < 127; i++)
				{
					if(strncmp(&pLine[i], " has requested to swap with you. To complete the swap process please wait ", 74) == 0)
					{
						nameEnd = i;
						break;
					}
				}
				if(nameEnd != 0)
				{
					m_AutoSwapTargetGuess = -1;
					for(int i = 0; i < MAX_CLIENTS; i++)
					{
						CGameClient::CClientData *client = &GameClient()->m_aClients[i];
						if(!client->m_Active)
							continue;
						if(!strncmp(client->m_aName, pLine, nameEnd) && client->m_aName[nameEnd] == '\0')
						{
							m_AutoSwapDummyGuess = g_Config.m_ClDummy;
							m_AutoSwapTargetGuess = i;
							break;
						}
					}
				}
			}
			// auto swap init -> idle (new request)
			if(m_AutoSwapState == AUTOSWAP_INIT && !strncmp(pLine, "You have requested to swap with ", 32))
			{
				char *name = GameClient()->m_aClients[m_AutoSwapTarget].m_aName;
				int nameLen = strlen(name);
				if(!strncmp(&pLine[32], name, nameLen) && (!strcmp(&pLine[32 + nameLen], ".") || !strcmp(&pLine[32 + nameLen], ". Use /cancelswap to cancel the request.")))
				{
					GameClient()->m_Chat.Echo("Auto swap finished because there was no pending swap request to accept");
					m_AutoSwapState = AUTOSWAP_IDLE;
				}
			}
			// auto swap init -> idle (already requested)
			if(m_AutoSwapState == AUTOSWAP_INIT && !strncmp(pLine, "You have already requested to swap with ", 40))
			{
				char *name = GameClient()->m_aClients[m_AutoSwapTarget].m_aName;
				int nameLen = strlen(name);
				if(!strncmp(&pLine[40], name, nameLen) && !strcmp(&pLine[40 + nameLen], "."))
				{
					GameClient()->m_Chat.Echo("Auto swap finished because there was no pending swap request to accept");
					m_AutoSwapState = AUTOSWAP_IDLE;
				}
			}
			// auto swap * -> idle (accepted)
			if(!str_find(pLine, " has swapped with "))
			{
				char *name = GameClient()->m_aClients[GameClient()->m_aLocalIds[0]].m_aName;
				int nameLen = strlen(name);
				if(!strncmp(&pLine[0], name, nameLen) && !strncmp(&pLine[0 + nameLen], " has swapped with ", 18))
					m_AutoSwapState = AUTOSWAP_IDLE;
				else
				{
					for(int i = 1; i < MAX_NAME_LENGTH; i++)
					{
						if(!strncmp(&pLine[0 + i], " has swapped with ", 18) && !strncmp(&pLine[0 + i + 18], name, nameLen))
						{
							m_AutoSwapState = AUTOSWAP_IDLE;
							break;
						}
					}
				}
			}
			// auto swap init -> delay
			if(m_AutoSwapState == AUTOSWAP_INIT && !strncmp(pLine, "You have to wait ", 17))
			{
				int numStart = 17;
				int secNum = pLine[numStart] - '0';
				if(secNum >= 0 && secNum <= 9)
				{
					while(true)
					{
						numStart++;
						if(pLine[numStart] >= '0' && pLine[numStart] <= '9')
							secNum = secNum * 10 + (pLine[numStart] - '0');
						else
							break;
					}
					if(!strcmp(&pLine[numStart], " seconds until you can swap."))
					{
						m_AutoSwapState = AUTOSWAP_DELAY;
						m_AutoSwapDelayExpire = time() + ((secNum + 1) * time_freq());
					}
				}
			}
		}
	}
}

void CCMClient::OnConsoleInit()
{
	Console()->Register("cm_autoteam", "", CFGFLAG_CLIENT, ConAutoTeam, this, "Join the team that a player most recently joined");
	Console()->Register("cm_autoswap", "?s[target]", CFGFLAG_CLIENT, ConAutoSwap, this, "Attempt to automatically accept a swap request");
	Console()->Register("cm_dump_skin", "s[target]", CFGFLAG_CLIENT, ConDumpSkin, this, "Print the skin and color of a player");
}
