using Godot;

using Sfs3.Client.Core;
using Sfs3.Client.Entities;
using Sfs3.Client.Entities.Data;
using Sfs3.Client.Entities.Variables;
using Sfs3.Client.Requests;
using Sfs3.Client.Requests.Buddylist;
using Sfs3.Client.Util.Monitor;

using System.Collections.Generic;

/**
 * Script attached to the main node in the Game scene.
 */
public partial class GameController : BaseController
{
	//----------------------------------------------------------
	// UI elements
	//----------------------------------------------------------

	[ExportCategory("Game Settings")]

	[Export]
	public GameEngine gameEngine;

	[ExportCategory("UI Settings")]

	[Export]
	public Control preGameUI;
	[Export]
	public Control midGameUI;
	[Export]
	public Control postGameUI;
	[Export]
	public Control shipOptionsContainer;
	[Export]
	public Label waitingLabel;
	[Export]
	public Control buddyInvitePanel;
	[Export]
	public Control buddyInviteList;
	[Export]
	public Label rankingLabel;

	//----------------------------------------------------------
	// Private properties
	//----------------------------------------------------------
	private ISFSObject level;
	private ISFSObject starshipModels;
	private ISFSObject weaponModels;
	private string selectedShipModel;
	private float countdownDelay;
	private bool countdownRunning;
	private Dictionary<string, BuddyInviteItem> buddyInviteItems;
	private int clientServerLag;

	// USER VARIABLES
	private const string UV_MODEL = "sModel";       // Starship model
	private const string UV_X = "x";                // x position
	private const string UV_Y = "y";                // y position
	private const string UV_VX = "vx";              // Velocity x component
	private const string UV_VY = "vy";              // Velocity y component
	private const string UV_DIR = "d";              // Ship direction
	private const string UV_THRUST = "t";           // Ship's thruster is active
	private const string UV_ROTATE = "r";           // Ship rotating direction
	private const string UV_SHIELD = "s";           // Ship's shield energy
	private const string USERVAR_EXPERIENCE = "xp"; // User experience

	// MMOITEM VARIABLES
	private const string IV_TYPE = "iType";         // MMOItem type
	private const string IV_MODEL = "iModel";       // MMOItem model
	private const string IV_X = "x";                // x position
	private const string IV_Y = "y";                // y position
	private const string IV_VX = "vx";              // Velocity x component
	private const string IV_VY = "vy";              // Velocity y component

	// MMOITEM TYPES
	private const string ITYPE_WEAPON = "weapon";   // MMOItem of type weapon

	// REQUESTS TO SERVER
	private const string REQ_READY = "init.ready";
	private const string REQ_SHIP = "init.ship";
	private const string REQ_INVITE = "init.invite";
	private const string REQ_ROTATE = "control.rotate";
	private const string REQ_THRUST = "control.thrust";
	private const string REQ_FIRE = "control.fire";

	// RESPONSES FROM SERVER
	private const string RESP_CONFIG = "config";
	private const string RESP_COUNTDOWN = "count";
	private const string RESP_LEAVE = "leave";
	private const string RESP_START = "start";
	private const string RESP_SHOT_XPLODE = "shot_xplode";
	private const string RESP_SHIP_XPLODE = "ship_xplode";
	private const string RESP_GAME_OVER = "game_over";

	//----------------------------------------------------------
	// Callback methods
	//----------------------------------------------------------
	#region
	public override void _Ready()
	{
		base._Ready();

		// Retrieve the SmartFox client instance created in the Login scene
		sfs = Global.GetSfsClient();

		// Add event listeners
		// This controller is in charge of handling all pre-game and post-game flow, communicating with the Room Extension
		// (starship selection, waiting for other players, inviting buddies, showing game over screen, etc)
		AddSmartFoxListeners();

		// Add listeners to events fired by the ShipMenuOption instances
		foreach (ShipMenuOption smo in GetShipMenuOptions())
			smo.SelectAction = OnShipSelected;

		// Show pre-game UI
		preGameUI.Visible = true;

		// Show game state
		ShowGameState();

		// Initialize buddy invitation panel
		InitBuddyInvitePanel();

		// Send READY message to Room Extension
		sfs.Send(new ExtensionRequest(REQ_READY, new SFSObject(), sfs.LastJoinedRoom));
	}

	public override void _Process(double delta)
	{
		base._Process(delta);

		if (countdownRunning)
		{
			if (countdownDelay > 0)
			{
				// Show progress
				waitingLabel.Text = "Game will start in " + Mathf.CeilToInt(countdownDelay).ToString("00") + " seconds";

				countdownDelay -= (float)delta;
			}
			else
			{
				countdownDelay = 0;
				countdownRunning = false;
			}
		}
	}
	#endregion

	//----------------------------------------------------------
	// UI event listeners
	//----------------------------------------------------------
	#region
	/**
	 * On Leave button click, go back to Lobby scene.
	 */
	public void OnLeaveButtonClick()
	{
		// Leave current room
		sfs.Send(new LeaveRoomRequest());

		// Leave game scene
		LeaveGameScene();
	}

	/**
	 * On custom event fired by a Ship Menu Option, send the starship selection to the Room Extension.
	 */
	public void OnShipSelected(string shipModel)
	{
		// Deselect previously selected ship
		if (selectedShipModel != null)
			foreach (ShipMenuOption smo in GetShipMenuOptions())
				if (smo.ShipName == selectedShipModel)
				{
					smo.Deselect();
					break;
				}

		// Save selection
		selectedShipModel = shipModel;

		ISFSObject data = new SFSObject();
		data.PutString("model", shipModel);

		// Send selection to Extension
		sfs.Send(new ExtensionRequest(REQ_SHIP, data, sfs.LastJoinedRoom));
	}

	/**
	 * On Invite button press in Buddy Invite Item, send the invitation request to the Room Extension.
	 */
	public void OnInviteBuddyButtonClick(BuddyInviteItem buddyInviteItem, Buddy buddy)
	{
		// Disable button
		buddyInviteItem.SetInviteEnabled(false);

		// Send request to Extension which, in turn, will send the invitation to the buddy
		ISFSObject data = new SFSObject();
		data.PutString("buddy", buddy.Name);

		sfs.Send(new ExtensionRequest(REQ_INVITE, data, sfs.LastJoinedRoom));
	}
	#endregion

	//----------------------------------------------------------
	// Game event listeners
	//----------------------------------------------------------
	#region
	/**
	 * Send the rotation event to the Room Extension.
	 */
	private void Rotate(int rotationDir)
	{
		ISFSObject paramsDir = new SFSObject();
		paramsDir.PutInt("dir", rotationDir);
		sfs.Send(new ExtensionRequest(REQ_ROTATE, paramsDir, sfs.LastJoinedRoom));
	}

	/**
	 * Send the thrust event to the Room Extension.
	 */
	private void Thrust(bool activate)
	{
		ISFSObject paramsThrust = new SFSObject();
		paramsThrust.PutBool("go", activate);
		sfs.Send(new ExtensionRequest(REQ_THRUST, paramsThrust, sfs.LastJoinedRoom));
	}

	/**
	 * Send the fire event to the Room Extension.
	 */
	private void Fire(int weaponNum)
	{
		ISFSObject paramsFire = new SFSObject();
		paramsFire.PutInt("wnum", weaponNum);
		sfs.Send(new ExtensionRequest(REQ_FIRE, paramsFire, sfs.LastJoinedRoom));
	}
	#endregion

	//----------------------------------------------------------
	// Helper methods
	//----------------------------------------------------------
	#region
	/**
	 * Add all SmartFoxServer-related event listeners required by the scene.
	 */
	private void AddSmartFoxListeners()
	{
		sfs.AddEventListener(SFSEvent.PING_PONG, OnPingPong);
		sfs.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse);
		sfs.AddEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVarsUpdate);
		sfs.AddEventListener(SFSEvent.PROXIMITY_LIST_UPDATE, OnProximityListUpdate);

		sfs.AddEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, OnBuddyOnlineStateChange);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, OnBuddyVariablesUpdate);
		sfs.AddEventListener(SFSBuddyEvent.BUDDY_MESSAGE, OnBuddyMessage);
	}

	/**
	 * Remove all SmartFoxServer-related event listeners added by the scene.
	 * This method is called by the parent BaseController when the scene is destroyed.
	 */
	public override void RemoveSmartFoxListeners()
	{
		if (sfs != null)
		{
			sfs.RemoveEventListener(SFSEvent.PING_PONG, OnPingPong);
			sfs.RemoveEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse);
			sfs.RemoveEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVarsUpdate);
			sfs.RemoveEventListener(SFSEvent.PROXIMITY_LIST_UPDATE, OnProximityListUpdate);

			sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, OnBuddyOnlineStateChange);
			sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, OnBuddyVariablesUpdate);
			sfs.RemoveEventListener(SFSBuddyEvent.BUDDY_MESSAGE, OnBuddyMessage);
		}
	}

	/**
	 * Hide all modal panels.
	 */
	protected override void HideModals()
	{
		// No modal panels in this scene
	}

	private IEnumerable<ShipMenuOption> GetShipMenuOptions()
	{
		foreach (Node child in shipOptionsContainer.GetChildren())
			if (child is ShipMenuOption smo)
				yield return smo;
	}

	private void ShowGameState()
	{
		// Check if game is already started
		int gameState = (int)sfs.LastJoinedRoom.GetVariable("gameState").IntValue;

		if (gameState == 0)
			waitingLabel.Text = "Waiting for players...";
		else if (gameState == 3)
			waitingLabel.Text = "Game already over";
	}

	private void LeaveGameScene()
	{
		// Set user as "Available" in Buddy List system
		if (sfs.BuddyManager.MyOnlineState)
			sfs.Send(new SetBuddyVariablesRequest([new SFSBuddyVariable(ReservedBuddyVariables.BV_STATE, "Available", VariableType.STRING)]));

		// Return to lobby scene
		GetTree().ChangeSceneToFile("res://scenes/Lobby.tscn");
	}

	private void InitBuddyInvitePanel()
	{
		bool isOnline = sfs.BuddyManager.MyOnlineState;

		// Init buddy-related data structures
		buddyInviteItems = [];

		// Update the buddy list
		if (isOnline)
		{
			// For the buddy list we use a scrollable area containing a separate instance of the Buddy Invite Item scene for each buddy
			// The item contains a clickable button to invite a buddy to play
			IList<Buddy> buddies = sfs.BuddyManager.BuddyList;

			// Display buddy invite items
			foreach (Buddy buddy in buddies)
				SetBuddyInviteItem(buddy);
		}

		// Display panel
		buddyInvitePanel.Visible = isOnline;
	}

	private void SetBuddyInviteItem(Buddy buddy)
	{
		string buddyName = buddy.Name;

		// Get reference to buddy invite item corresponding to Buddy
		buddyInviteItems.TryGetValue(buddyName, out BuddyInviteItem buddyInviteItem);

		if (buddyInviteItem != null)
		{
			// If buddy went offline or is not available anymore, remove it
			if (!buddy.IsOnline || buddy.IsBlocked || buddy.State != "Available")
			{
				// Remove buddy invite item from dictionary
				buddyInviteItems.Remove(buddyName);

				// Destroy node
				buddyInviteList.RemoveChild(buddyInviteItem);
				buddyInviteItem.QueueFree();
			}
		}
		else
		{
			// If buddy is online, not blocked and available, add it
			if (buddy.IsOnline && !buddy.IsBlocked && buddy.State == "Available")
			{
				// Create buddy invite item
				buddyInviteItem = ResourceLoader.Load<PackedScene>("res://scenes/sub/BuddyInviteItem.tscn").Instantiate<BuddyInviteItem>();
				buddyInviteItems.Add(buddyName, buddyInviteItem);

				// Set buddy name
				buddyInviteItem.nameLabel.Text = buddy.DisplayName;

				// Add buddy invite item to container
				buddyInviteList.AddChild(buddyInviteItem);

				// Add listener to button
				buddyInviteItem.inviteButton.Pressed += () => { OnInviteBuddyButtonClick(buddyInviteItem, buddy); };
			}
		}
	}

	private string ToOrdinal(long number)
	{
		if (number < 0)
			return number.ToString();

		long rem = number % 100;

		if (rem >= 11 && rem <= 13)
			return number + "th";

		switch (number % 10)
		{
			case 1:
				return number + "st";
			case 2:
				return number + "nd";
			case 3:
				return number + "rd";
			default:
				return number + "th";
		}
	}
	#endregion

	//----------------------------------------------------------
	// SmartFoxServer event listeners
	//----------------------------------------------------------
	#region
	private void OnExtensionResponse(ApiEvent evt)
	{
		string cmd = (string)evt.Params[EventParam.Cmd];
		ISFSObject data = (SFSObject)evt.Params[EventParam.ExtParams];

		if (cmd == RESP_CONFIG)
		{
			Global.Trace("Starships configuration received");

			// Retrieve planet settings, starship models and weapon models from custom data sent by the Room Extension
			level = data.GetSFSObject("level");
			starshipModels = data.GetSFSObject("starships");
			weaponModels = data.GetSFSObject("weapons");

			// Display starships' stats
			foreach (ShipMenuOption smo in GetShipMenuOptions())
			{
				string name = smo.ShipName;
				ISFSObject stats = starshipModels.GetSFSObject(name);

				smo.SetSpeedStat((int)stats.GetInt("maxSpeed"), 200);
				smo.SetRotationStat((int)stats.GetInt("rotationSpeed"), 200);
				smo.SetThrustStat((int)stats.GetInt("thrustAccel"), 150);
				smo.SetWeaponName(stats.GetString("weapon1") + "," + stats.GetString("weapon2"));
			}
		}

		else if (cmd == RESP_LEAVE)
		{
			Global.Trace("Game aborted due to minimum number of players not yet reached; leaving game scene");

			LeaveGameScene();
		}

		else if (cmd == RESP_COUNTDOWN)
		{
			Global.Trace("Begin game start countdown; game will start in " + data.GetInt("delay") + "s");

			// Set initial countdown delay
			countdownDelay = (int)data.GetInt("delay");
			countdownRunning = true;
		}

		else if (cmd == RESP_START)
		{
			Global.Trace("Game start!");

			// Hide pre-game UI
			preGameUI.Visible = false;

			// Show mid-game UI
			midGameUI.Visible = true;

			// Create planet
			gameEngine.CreatePlanet(level);

			// Register listeners
			gameEngine.StarshipRotate += Rotate;
			gameEngine.StarshipThrust += Thrust;
			gameEngine.StarshipFire += Fire;
		}

		else if (cmd == RESP_SHOT_XPLODE)
		{
			// A weapon shot exploded

			int shotId = (int)data.GetInt("id");
			int posX = (int)data.GetInt("x");
			int posY = (int)data.GetInt("y");

			gameEngine.RemoveWeaponShot(shotId, true, posX, posY);
		}

		else if (cmd == RESP_SHIP_XPLODE)
		{
			// A starship exploded

			int shipId = (int)data.GetInt("id");

			gameEngine.RemoveStarship(shipId, true);
		}

		else if (cmd == RESP_GAME_OVER)
		{
			Global.Trace("Game over");

			// It's game over

			int ranking = (int)data.GetInt("r");
			bool isTie = (bool)data.GetBool("t");

			// De-register listeners
			gameEngine.StarshipRotate -= Rotate;
			gameEngine.StarshipThrust -= Thrust;
			gameEngine.StarshipFire -= Fire;

			// Remove my starship
			gameEngine.RemoveStarship(sfs.MySelf.Id, false);

			// Hide mid-game UI
			midGameUI.Visible = false;

			// Show post-game UI
			postGameUI.Visible = true;

			// Display ranking
			if (ranking == 1)
			{
				if (!isTie)
					rankingLabel.Text = "YOU ARE THE WINNER";
				else
					rankingLabel.Text = "IT'S A TIE";
			}
			else
			{
				if (!isTie)
					rankingLabel.Text = "You ranked " + ToOrdinal(ranking);
				else
					rankingLabel.Text = "You tied for " + ToOrdinal(ranking) + " place";
			}

			rankingLabel.Text += "\nYour XP is " + sfs.MySelf.GetVariable(USERVAR_EXPERIENCE).IntValue;
		}
	}

	/**
	 * Synchronize the user trajectory with the server-side simulation.
	 * This requires the client-server lag compensation: in other words the elapsed time since the event was sent
	 * by the server is taken into account to guess where the starship is located now.
	 */
	private void OnUserVarsUpdate(ApiEvent evt)
	{
		User user = (User)evt.Params[EventParam.User];
		List<string> changedVars = (List<string>)evt.Params[EventParam.ChangedVars];

		if (changedVars.Contains(UV_ROTATE))
		{
			// Make user starship start or stop rotating (excluding current user who controls his starship directly)
			if (!user.IsItMe)
			{
				int r1 = (int)user.GetVariable(UV_ROTATE).IntValue;
				gameEngine.SetStarshipRotating(user.Id, r1);
			}
		}

		if (changedVars.Contains(UV_X) || changedVars.Contains(UV_Y) ||
			changedVars.Contains(UV_VX) || changedVars.Contains(UV_VY) ||
			changedVars.Contains(UV_DIR) || changedVars.Contains(UV_THRUST))
		{
			// Create current user starship if not yet existing
			if (user.IsItMe)
			{
				string shipModel = user.GetVariable(UV_MODEL).StringValue;
				gameEngine.CreateStarship(user.Id, user.Name, true, shipModel, starshipModels.GetSFSObject(shipModel));
			}

			// Reset user starship state in simulator, taking lag into account
			float x = (float)user.GetVariable(UV_X).DoubleValue;
			float y = (float)user.GetVariable(UV_Y).DoubleValue;
			float vx = (float)user.GetVariable(UV_VX).DoubleValue;
			float vy = (float)user.GetVariable(UV_VY).DoubleValue;
			float d = (float)user.GetVariable(UV_DIR).DoubleValue;
			int s = (int)user.GetVariable(UV_SHIELD).IntValue;
			bool t = (bool)user.GetVariable(UV_THRUST).BoolValue;

			gameEngine.SetStarshipPosition(user.Id, x, y, vx, vy, d, t, s, clientServerLag);
		}
	}

	/**
	 * Create/remove the starships of users entering/leaving the current user's Area of Interest (AoI).
	 * Create/remove the weapon shots corresponding to MMOItems entering/leaving the current user's Area of Interest (AoI).
	 */
	private void OnProximityListUpdate(ApiEvent evt)
	{
		// Loop the removedUsers list in the event params to remove the starships no more visible
		List<User> removedUsers = (List<User>)evt.Params[EventParam.RemovedUsers];

		foreach (User ru in removedUsers)
			gameEngine.RemoveStarship(ru.Id, false);

		// Loop the addedUsers list in the event params to create the starships now visible
		// To the usual lag we add 10ms, which is half the value of the proximityListUpdateMillis setting on the server
		// As we don't know exactly after how much time the update event was fired after the users updated their positions in the MMORoom
		// (could be 0ms up to 20ms), we use half the proximityListUpdateMillis value as a sort of mean value for an additional correction of the lag
		List<User> addedUsers = (List<User>)evt.Params[EventParam.AddedUsers];

		foreach (User au in addedUsers)
		{
			// Create starship
			string shipModel = au.GetVariable(UV_MODEL).StringValue;
			gameEngine.CreateStarship(au.Id, au.Name, false, shipModel, starshipModels.GetSFSObject(shipModel));

			// Get position-related User Variables
			float x = (float)au.GetVariable(UV_X).DoubleValue;
			float y = (float)au.GetVariable(UV_Y).DoubleValue;
			float vx = (float)au.GetVariable(UV_VX).DoubleValue;
			float vy = (float)au.GetVariable(UV_VY).DoubleValue;
			float d = (float)au.GetVariable(UV_DIR).DoubleValue;
			bool t = (bool)au.GetVariable(UV_THRUST).BoolValue;
			int r = (int)au.GetVariable(UV_ROTATE).IntValue;
			int s = (int)au.GetVariable(UV_SHIELD).IntValue;

			// Set starship rotating flag
			gameEngine.SetStarshipRotating(au.Id, r);

			// Set starship position
			gameEngine.SetStarshipPosition(au.Id, x, y, vx, vy, d, t, s, clientServerLag + 10);
		}

		// Loop the removedItems list in the event params to remove the weapon shots no more visible
		// NOTE: sprites might have been removed already, in case the shots exploded within the AoI of the user
		// (notified by a dedicated Extension response)
		List<IMMOItem> removedItems = (List<IMMOItem>)evt.Params[EventParam.RemovedItems];

		foreach (IMMOItem ri in removedItems)
			gameEngine.RemoveWeaponShot(ri.Id, false);

		// Loop the addedItems list in the event params to create those now visible
		// The same note about addedUsers applies here
		List<IMMOItem> addedItems = (List<IMMOItem>)evt.Params[EventParam.AddedItems];

		foreach (IMMOItem ai in addedItems)
		{
			string type = ai.GetVariable(IV_TYPE).StringValue;

			if (type == ITYPE_WEAPON)
			{
				// Get position-related MMOItem Variables
				string im = ai.GetVariable(IV_MODEL).StringValue;
				float ix = (float)ai.GetVariable(IV_X).DoubleValue;
				float iy = (float)ai.GetVariable(IV_Y).DoubleValue;
				float ivx = (float)ai.GetVariable(IV_VX).DoubleValue;
				float ivy = (float)ai.GetVariable(IV_VY).DoubleValue;

				// Create weapon shot
				gameEngine.CreateWeaponShot(ai.Id, weaponModels.GetSFSObject(im), ix, iy, ivx, ivy, clientServerLag + 10);
			}
		}
	}

	/**
	 * Evaluate the current client-server lag.
	 * Returned value is divided by two because we just need the server to client lag (not client to server to client).
	 */
	private void OnPingPong(ApiEvent evt)
	{
		LagValue lagValue = (LagValue)evt.Params[EventParam.LagValue];

		clientServerLag = (int)lagValue.Average / 2;
	}

	private void OnBuddyOnlineStateChange(ApiEvent evt)
	{
		Buddy buddy = (Buddy)evt.Params[EventParam.Buddy];
		bool isItMe = (bool)evt.Params[EventParam.IsItMe];

		if (!isItMe)
			SetBuddyInviteItem(buddy);
	}

	private void OnBuddyVariablesUpdate(ApiEvent evt)
	{
		Buddy buddy = (Buddy)evt.Params[EventParam.Buddy];
		bool isItMe = (bool)evt.Params[EventParam.IsItMe];

		if (!isItMe)
			SetBuddyInviteItem(buddy);
	}

	private void OnBuddyMessage(ApiEvent evt)
	{
		// No need to check if the sender is the current user or a buddy,
		// as in this example the current user can't send buddy messages from the Game scene

		Buddy sender = (Buddy)evt.Params[EventParam.Buddy];
		string message = (string)evt.Params[EventParam.Message];

		// Add message to queue under the sender's name (the buddy the conversation is with)
		BuddyChatMessage chatMsg = BuddyChatHistoryManager.AddMessage(sender.Name, sender.DisplayName, false, message);

		// Increase unread messages count
		BuddyChatHistoryManager.IncreaseUnreadMessagesCount(chatMsg.Target);
	}
	#endregion
}
