import * as bootstrap from 'bootstrap';
import BaseController from './base-controller.js';
import BuddyChatHistoryManager from '../managers/buddy-chat-history-manager.js';
import ShipOption from '../components/ship-option.js';
import BuddyInviteList from '../components/buddy-invite-list.js';
import GameManager from '../managers/game-manager.js';
import { SFSObject, SFSEvent, SFSBuddyEvent, EventParam, SFSBuddyVariable, VariableType, ReservedBuddyVariables } from 'sfs3-api';
import { LeaveRoomRequest, ExtensionRequest, SetBuddyVariablesRequest } from 'sfs3-api';

export default class GameController extends BaseController
{
	static USERVAR_EXPERIENCE = 'xp';

	// User Variables (starship state)
	static UV_MODEL = 'sModel';
	static UV_X = 'x';
	static UV_Y = 'y';
	static UV_VX = 'vx';
	static UV_VY = 'vy';
	static UV_DIR = 'd';
	static UV_THRUST = 't';
	static UV_ROTATE = 'r';
	static UV_SHIELD = 's';

	// MMOItem Variables (weapon shots)
	static IV_TYPE = 'iType';
	static IV_MODEL = 'iModel';
	static IV_X = 'x';
	static IV_Y = 'y';
	static IV_VX = 'vx';
	static IV_VY = 'vy';
	static ITYPE_WEAPON = 'weapon';

	// Requests to the Room Extension
	static REQ_READY = 'init.ready';
	static REQ_SHIP = 'init.ship';
	static REQ_INVITE = 'init.invite';
	static REQ_ROTATE = 'control.rotate';
	static REQ_THRUST = 'control.thrust';
	static REQ_FIRE = 'control.fire';

	// Responses from the Room Extension
	static RESP_CONFIG = 'config';
	static RESP_COUNTDOWN = 'count';
	static RESP_LEAVE = 'leave';
	static RESP_START = 'start';
	static RESP_SHOT_XPLODE = 'shot_xplode';
	static RESP_SHIP_XPLODE = 'ship_xplode';
	static RESP_GAME_OVER = 'game_over';

	constructor(app, view, name)
	{
		super(app, view, name);

		//------------------------------------------------------

		this.preGameUI = view.querySelector('#preGameUI');
		this.gameArea = view.querySelector('#gameArea');
		this.gameContainer = view.querySelector('#gameContainer');
		this.postGameUI = view.querySelector('#postGameUI');
		this.waitingText = view.querySelector('#waitingText');
		this.rankingText = view.querySelector('#rankingText');
		this.shipOptions = view.querySelectorAll('ship-option');
		this.shipTab = view.querySelector('#shipTab');
		this.inviteTab = view.querySelector('#inviteTab');
		this.invitePane = view.querySelector('#invitePane');
		this.buddyInviteList = view.querySelector('#buddyInviteList');
		this.leaveGameButton = view.querySelector('#leaveGameButton');
	}

	//----------------------------------------------------------
	// Overridden methods
	//----------------------------------------------------------

	initialize()
	{
		super.initialize();

		// Reset state
		this.selectedShipModel = null;
		this.level = null;
		this.starshipModels = null;
		this.weaponModels = null;
		this.clientServerLag = 0;

		// Show pre-game UI, hide the others
		this.preGameUI.hidden = false;
		this.gameArea.hidden = true;
		this.postGameUI.hidden = true;

		// Add leave game button click listener
		this.boundOnLeaveGameButtonClick = this.onLeaveGameButtonClick.bind(this);
		this.leaveGameButton.addEventListener('click', this.boundOnLeaveGameButtonClick);

		// Add ship selection listeners
		this.boundOnShipSelect = this.onShipSelect.bind(this);
		this.shipOptions.forEach(opt => opt.addEventListener(ShipOption.SHIP_SELECT_EVT, this.boundOnShipSelect));

		// Add buddy invite listener
		this.boundOnBuddyInviteClick = this.onBuddyInviteClick.bind(this);
		this.buddyInviteList.addEventListener(BuddyInviteList.INVITE_CLICK_EVT, this.boundOnBuddyInviteClick);

		// Add SFS event listeners
		this.sfs.addEventListener(SFSEvent.EXTENSION_RESPONSE, this.onExtensionResponse, this);
		this.sfs.addEventListener(SFSEvent.PING_PONG, this.onPingPong, this);
		this.sfs.addEventListener(SFSEvent.USER_VARIABLES_UPDATE, this.onUserVarsUpdate, this);
		this.sfs.addEventListener(SFSEvent.PROXIMITY_LIST_UPDATE, this.onProximityListUpdate, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, this.onBuddyOnlineStateChange, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, this.onBuddyVariablesUpdate, this);
		this.sfs.addEventListener(SFSBuddyEvent.BUDDY_MESSAGE, this.onBuddyMessage, this);

		// Show current game state
		this.showGameState();

		// Initialize buddy invitation panel
		this.initBuddyInvitePanel();

		// Create the game manager (the PixiJS application is initialized when the game starts)
		this.gameManager = new GameManager(this.sfs, this.gameContainer);

		// Forward the player controls dispatched by the game manager to the Room Extension
		this.boundOnEngineRotate = this.onEngineRotate.bind(this);
		this.boundOnEngineThrust = this.onEngineThrust.bind(this);
		this.boundOnEngineFire = this.onEngineFire.bind(this);
		this.gameManager.addEventListener(GameManager.ROTATE_EVT, this.boundOnEngineRotate);
		this.gameManager.addEventListener(GameManager.THRUST_EVT, this.boundOnEngineThrust);
		this.gameManager.addEventListener(GameManager.FIRE_EVT, this.boundOnEngineFire);

		// Tell the Room Extension the user is ready
		this.sfs.send(new ExtensionRequest(GameController.REQ_READY, new SFSObject(), this.sfs.lastJoinedRoom));

		// Show view
		this.showView();
	}

	destroy()
	{
		super.destroy(); // This also calls removeSmartFoxListeners method!

		// Stop countdown
		clearInterval(this.countdownInterval);

		// Remove game manager control listeners and destroy it
		this.gameManager.removeEventListener(GameManager.ROTATE_EVT, this.boundOnEngineRotate);
		this.gameManager.removeEventListener(GameManager.THRUST_EVT, this.boundOnEngineThrust);
		this.gameManager.removeEventListener(GameManager.FIRE_EVT, this.boundOnEngineFire);
		this.gameManager.destroy();
		this.gameManager = null;

		// Remove leave game button click listener
		this.leaveGameButton.removeEventListener('click', this.boundOnLeaveGameButtonClick);

		// Remove ship selection listeners
		this.shipOptions.forEach(opt => opt.removeEventListener(ShipOption.SHIP_SELECT_EVT, this.boundOnShipSelect));

		// Remove buddy invite listener
		this.buddyInviteList.removeEventListener(BuddyInviteList.INVITE_CLICK_EVT, this.boundOnBuddyInviteClick);

		// Reset ship selection and buddy invite list
		this.shipOptions.forEach(opt => opt.setSelected(false));
		this.buddyInviteList.reset();
	}

	removeSmartFoxListeners()
	{
		if (this.sfs != null)
		{
			this.sfs.removeEventListener(SFSEvent.EXTENSION_RESPONSE, this.onExtensionResponse);
			this.sfs.removeEventListener(SFSEvent.PING_PONG, this.onPingPong);
			this.sfs.removeEventListener(SFSEvent.USER_VARIABLES_UPDATE, this.onUserVarsUpdate);
			this.sfs.removeEventListener(SFSEvent.PROXIMITY_LIST_UPDATE, this.onProximityListUpdate);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_ONLINE_STATE_CHANGE, this.onBuddyOnlineStateChange);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_VARIABLES_UPDATE, this.onBuddyVariablesUpdate);
			this.sfs.removeEventListener(SFSBuddyEvent.BUDDY_MESSAGE, this.onBuddyMessage);
		}
	}

	//----------------------------------------------------------
	// Other methods
	//----------------------------------------------------------

	showGameState()
	{
		// Check if the game is already started
		const gameStateVar = this.sfs.lastJoinedRoom.getVariable('gameState');
		const gameState = gameStateVar != null ? gameStateVar.value : 0;

		if (gameState == 0)
			this.waitingText.innerText = 'Waiting for players...';
		else if (gameState == 3)
			this.waitingText.innerText = 'Game already over';
	}

	initBuddyInvitePanel()
	{
		const isOnline = this.sfs.buddyManager.myOnlineState;

		// Reset list
		this.buddyInviteList.reset();

		// Display buddy invite items
		if (isOnline)
			for (let buddy of this.sfs.buddyManager.buddyList)
				this.setBuddyInviteItem(buddy);

		// Show the invite tab and panel only if the user is online in the Buddy List system
		this.inviteTab.hidden = !isOnline;
		this.invitePane.hidden = !isOnline;

		// Make sure the ship panel is the active tab
		bootstrap.Tab.getOrCreateInstance(this.shipTab).show();
	}

	/**
	 * Add or remove a buddy invite item depending on the buddy availability.
	 */
	setBuddyInviteItem(buddy)
	{
		const available = buddy.isOnline && !buddy.isBlocked && buddy.state == 'Available';

		if (this.buddyInviteList.hasItem(buddy.name))
		{
			// If the buddy is not available anymore, remove it
			if (!available)
				this.buddyInviteList.removeItem(buddy.name);
		}
		else
		{
			// If the buddy became available, add it
			if (available)
				this.buddyInviteList.addItem(buddy.name, buddy.displayName);
		}
	}

	leaveGameScene()
	{
		// Set user as "Available" in the Buddy List system
		if (this.sfs.buddyManager.myOnlineState)
		{
			const buddyVars = new Array();
			buddyVars.push(new SFSBuddyVariable(ReservedBuddyVariables.BV_STATE, 'Available', VariableType.STRING));
			this.sfs.send(new SetBuddyVariablesRequest(buddyVars));
		}

		// Return to lobby view
		this.app.goToView('lobby');
	}

	startCountdown(delay)
	{
		// Stop any running countdown
		clearInterval(this.countdownInterval);

		let secs = delay;

		const render = () => {
			this.waitingText.innerText = `Game will start in ${String(Math.max(0, secs)).padStart(2, '0')} seconds`;
		};

		render();

		this.countdownInterval = setInterval(() => {
			secs--;

			if (secs <= 0)
			{
				secs = 0;
				clearInterval(this.countdownInterval);
			}

			render();
		}, 1000);
	}

	/**
	 * Create a user's starship if it doesn't exist yet, provided its model (and settings) are available.
	 * Returns true if the starship exists after the call.
	 */
	ensureStarship(user, isMine)
	{
		if (this.gameManager == null || !this.gameManager.inited)
			return false;

		if (this.gameManager.hasStarship(user.id))
			return true;

		const modelVar = user.getVariable(GameController.UV_MODEL);
		if (modelVar == null)
			return false;

		const settings = this.starshipModels.getSFSObject(modelVar.value);
		if (settings == null)
			return false;

		this.gameManager.createStarship(user.id, user.name, isMine, modelVar.value, settings);
		return true;
	}

	/**
	 * Create the starships of all the users already in the game (including the current user).
	 * Called once the game engine is initialized, to cover the updates missed during the async init.
	 */
	createVisibleStarships()
	{
		if (this.gameManager == null || !this.gameManager.inited)
			return;

		const seen = new Set();

		// The current user first, then every other user currently in the Area of Interest
		for (const user of [this.sfs.mySelf, ...this.sfs.lastJoinedRoom.userList])
		{
			if (seen.has(user.id))
				continue;
			seen.add(user.id);

			// Skip users whose starship position is not set yet (e.g. spectators)
			if (user.getVariable(GameController.UV_X) == null)
				continue;

			if (!this.ensureStarship(user, user.isItMe))
				continue;

			this.gameManager.setStarshipRotating(user.id, user.getVariable(GameController.UV_ROTATE).value);
			this.gameManager.setStarshipPosition(
				user.id,
				user.getVariable(GameController.UV_X).value,
				user.getVariable(GameController.UV_Y).value,
				user.getVariable(GameController.UV_VX).value,
				user.getVariable(GameController.UV_VY).value,
				user.getVariable(GameController.UV_DIR).value,
				user.getVariable(GameController.UV_THRUST).value,
				user.getVariable(GameController.UV_SHIELD).value,
				this.clientServerLag
			);
		}
	}

	toOrdinal(n)
	{
		if (n < 0)
			return String(n);

		const rem = n % 100;

		if (rem >= 11 && rem <= 13)
			return n + 'th';

		switch (n % 10)
		{
			case 1: return n + 'st';
			case 2: return n + 'nd';
			case 3: return n + 'rd';
			default: return n + 'th';
		}
	}

	//----------------------------------------------------------
	// UI event listeners
	//----------------------------------------------------------

	/**
	 * On Leave button click, leave the game Room and return to the lobby view.
	 */
	onLeaveGameButtonClick(e)
	{
		// Leave current Room
		this.sfs.send(new LeaveRoomRequest());

		// Leave game scene
		this.leaveGameScene();
	}

	/**
	 * On starship selection, send the selection to the Room Extension.
	 */
	onShipSelect(e)
	{
		const shipModel = e.detail.ship;

		// Update selection highlight
		this.shipOptions.forEach(opt => opt.setSelected(opt.ship == shipModel));

		// Save selection
		this.selectedShipModel = shipModel;

		// Send selection to the Room Extension
		const data = new SFSObject();
		data.putString('model', shipModel);

		this.sfs.send(new ExtensionRequest(GameController.REQ_SHIP, data, this.sfs.lastJoinedRoom));
	}

	/**
	 * On buddy invite button click, ask the Room Extension to invite the buddy.
	 */
	onBuddyInviteClick(e)
	{
		const buddyName = e.detail.buddyName;

		// Send request to the Room Extension which, in turn, sends the invitation to the buddy
		const data = new SFSObject();
		data.putString('buddy', buddyName);

		this.sfs.send(new ExtensionRequest(GameController.REQ_INVITE, data, this.sfs.lastJoinedRoom));

		// If no more buddies can be invited, switch back to the ship panel
		if (!this.buddyInviteList.hasEnabledItems())
			bootstrap.Tab.getOrCreateInstance(this.shipTab).show();
	}

	//----------------------------------------------------------
	// SmartFoxServer event listeners
	//----------------------------------------------------------

	onExtensionResponse(evtParams)
	{
		const cmd = evtParams[EventParam.Cmd];
		const data = evtParams[EventParam.ExtParams];

		switch (cmd)
		{
			case GameController.RESP_CONFIG:
			{
				this.app.trace("Starships configuration received");

				// Retrieve planet settings, starship models and weapon models
				this.level = data.getSFSObject('level');
				this.starshipModels = data.getSFSObject('starships');
				this.weaponModels = data.getSFSObject('weapons');

				// Display starships stats
				this.shipOptions.forEach(opt => {
					const stats = this.starshipModels.getSFSObject(opt.ship);

					if (stats != null)
						opt.setStats(
							stats.getNumber('maxSpeed'),
							stats.getNumber('rotationSpeed'),
							stats.getNumber('thrustAccel'),
							stats.getString('weapon1') + ', ' + stats.getString('weapon2')
						);
				});

				break;
			}

			case GameController.RESP_LEAVE:
			{
				this.app.trace("Game aborted due to minimum number of players not yet reached; leaving game scene");

				this.leaveGameScene();

				break;
			}

			case GameController.RESP_COUNTDOWN:
			{
				this.app.trace("Begin game start countdown; game will start in " + data.getNumber("delay") + "s");

				// Begin the game start countdown
				this.startCountdown(data.getNumber('delay'));

				break;
			}

			case GameController.RESP_START:
			{
				this.app.trace("Game start!");

				// Stop countdown
				clearInterval(this.countdownInterval);

				// Hide pre-game UI and show the game canvas
				this.preGameUI.hidden = true;
				this.gameArea.hidden = false;

				// Initialize the PixiJS application, then create the starships already in the game
				// (the User Variables / proximity updates sent while the async init was running are missed otherwise)
				this.gameManager.init(this.level).then(() => this.createVisibleStarships());

				break;
			}

			case GameController.RESP_SHOT_XPLODE:
			{
				// A weapon shot exploded
				this.gameManager.removeWeaponShot(data.getNumber('id'), true, data.getNumber('x'), data.getNumber('y'));

				break;
			}

			case GameController.RESP_SHIP_XPLODE:
			{
				// A starship exploded
				this.gameManager.removeStarship(data.getNumber('id'), true);

				break;
			}

			case GameController.RESP_GAME_OVER:
			{
				this.app.trace("Game over");

				const ranking = data.getNumber('r');
				const isTie = data.getBool('t');

				// Remove the player's starship (the winner disappears), but keep the engine running so the
				// remaining weapon shots keep flying and any opponent explosion still plays
				if (this.gameManager.inited)
					this.gameManager.removeStarship(this.sfs.mySelf.id, false);

				// Show the ranking as an overlay above the still-running game
				this.postGameUI.hidden = false;

				// Display ranking
				let text;

				if (ranking == 1)
					text = isTie ? '<b>IT\'S A TIE</b>' : '<b>YOU ARE THE WINNER</b>';
				else
					text = isTie ? `<b>You tied for ${this.toOrdinal(ranking)} place</b>` : `<b>You ranked ${this.toOrdinal(ranking)}</b>`;

				const expVar = this.sfs.mySelf.getVariable(GameController.USERVAR_EXPERIENCE);
				if (expVar != null)
					text += `<br>Your XP is ${expVar.value}`;

				this.rankingText.innerHTML = text;

				break;
			}
		}
	}

	/**
	 * Synchronize a starship trajectory with the server-side simulation, taking the lag into account.
	 */
	onUserVarsUpdate(evtParams)
	{
		const user = evtParams[EventParam.User];
		const changedVars = evtParams[EventParam.ChangedVars];

		if (changedVars.includes(GameController.UV_ROTATE))
		{
			// Make other users' starships start/stop rotating (the current user controls their own directly)
			if (!user.isItMe)
				this.gameManager.setStarshipRotating(user.id, user.getVariable(GameController.UV_ROTATE).value);
		}

		if (changedVars.includes(GameController.UV_X) || changedVars.includes(GameController.UV_Y) ||
			changedVars.includes(GameController.UV_VX) || changedVars.includes(GameController.UV_VY) ||
			changedVars.includes(GameController.UV_DIR) || changedVars.includes(GameController.UV_THRUST))
		{
			// Create the starship if it doesn't exist yet (the update that created it may have been
			// missed while the game engine was still initializing)
			if (!this.ensureStarship(user, user.isItMe))
				return;

			this.gameManager.setStarshipPosition(
				user.id,
				user.getVariable(GameController.UV_X).value,
				user.getVariable(GameController.UV_Y).value,
				user.getVariable(GameController.UV_VX).value,
				user.getVariable(GameController.UV_VY).value,
				user.getVariable(GameController.UV_DIR).value,
				user.getVariable(GameController.UV_THRUST).value,
				user.getVariable(GameController.UV_SHIELD).value,
				this.clientServerLag
			);
		}
	}

	/**
	 * Create/remove the starships and weapon shots entering/leaving the current user's Area of Interest.
	 */
	onProximityListUpdate(evtParams)
	{
		if (this.gameManager == null || !this.gameManager.inited)
			return;

		// Loop the removedUsers list in the event params to remove the starships no more visible
		const removedUsers = evtParams[EventParam.RemovedUsers] || [];
		
		for (const ru of removedUsers)
			this.gameManager.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 corretion of the lag
		const addedUsers = evtParams[EventParam.AddedUsers] || [];

		for (const au of addedUsers)
		{
			if (!this.ensureStarship(au, au.isItMe))
				continue;

			this.gameManager.setStarshipRotating(au.id, au.getVariable(GameController.UV_ROTATE).value);

			this.gameManager.setStarshipPosition(
				au.id,
				au.getVariable(GameController.UV_X).value,
				au.getVariable(GameController.UV_Y).value,
				au.getVariable(GameController.UV_VX).value,
				au.getVariable(GameController.UV_VY).value,
				au.getVariable(GameController.UV_DIR).value,
				au.getVariable(GameController.UV_THRUST).value,
				au.getVariable(GameController.UV_SHIELD).value,
				this.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) 
		const removedItems = evtParams[EventParam.RemovedItems] || [];

		for (const ri of removedItems)
			this.gameManager.removeWeaponShot(ri.id, false);

		// Loop the addedItems list in the event params to create those now visible
		// The same note about addedUsers applies here
		const addedItems = evtParams[EventParam.AddedItems] || [];
		
		for (const ai of addedItems)
		{
			const type = ai.getVariable(GameController.IV_TYPE).value;

			if (type === GameController.ITYPE_WEAPON)
			{
				const model = ai.getVariable(GameController.IV_MODEL).value;
				this.gameManager.createWeaponShot(
					ai.id,
					this.weaponModels.getSFSObject(model),
					ai.getVariable(GameController.IV_X).value,
					ai.getVariable(GameController.IV_Y).value,
					ai.getVariable(GameController.IV_VX).value,
					ai.getVariable(GameController.IV_VY).value,
					this.clientServerLag + 10
				);
			}
		}
	}

	onPingPong(evtParams)
	{
		// 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)
		
		const lagValue = evtParams[EventParam.LagValue];
		this.clientServerLag = Math.floor(lagValue.average / 2);
	}

	onBuddyOnlineStateChange(evtParams)
	{
		const buddy = evtParams[EventParam.Buddy];
		const isItMe = evtParams[EventParam.IsItMe];

		if (!isItMe)
			this.setBuddyInviteItem(buddy);
	}

	onBuddyVariablesUpdate(evtParams)
	{
		const buddy = evtParams[EventParam.Buddy];
		const isItMe = evtParams[EventParam.IsItMe];

		if (!isItMe)
			this.setBuddyInviteItem(buddy);
	}

	onBuddyMessage(evtParams)
	{
		// 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 view

		const sender = evtParams[EventParam.Buddy];
		const message = evtParams[EventParam.Message];

		const chatMsg = BuddyChatHistoryManager.addMessage(sender.name, sender.nickname ? sender.nickname : sender.name, false, message);

		// Increase unread messages count
		BuddyChatHistoryManager.increaseUnreadMessagesCount(chatMsg.target);
	}

	//----------------------------------------------------------
	// Game engine control listeners
	//----------------------------------------------------------

	onEngineRotate(e)
	{
		const params = new SFSObject();
		params.putInt('dir', e.detail.dir);
		this.sfs.send(new ExtensionRequest(GameController.REQ_ROTATE, params, this.sfs.lastJoinedRoom));
	}

	onEngineThrust(e)
	{
		const params = new SFSObject();
		params.putBool('go', e.detail.activate);
		this.sfs.send(new ExtensionRequest(GameController.REQ_THRUST, params, this.sfs.lastJoinedRoom));
	}

	onEngineFire(e)
	{
		const params = new SFSObject();
		params.putInt('wnum', e.detail.fire);
		this.sfs.send(new ExtensionRequest(GameController.REQ_FIRE, params, this.sfs.lastJoinedRoom));
	}
}
