import BaseController from './base-controller.js';
import AssetManager from '../managers/asset-manager.js';
import { SFSObject, SFSEvent, EventParam, VariableType, Vec3D, SFSUserVariable } from 'sfs3-api';
import { LeaveRoomRequest, PublicMessageRequest, SetUserVariablesRequest, SetUserPositionRequest, ExtensionRequest } from 'sfs3-api';
import * as bootstrap from 'bootstrap';
import Avatar from '../objects/avatar.js';

export default class GameController extends BaseController
{
    constructor(app, view, name)
    {
        super(app, view, name);

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

        this.USERVAR_X = 'x';
        this.USERVAR_Y = 'y';
        this.USERVAR_DIR = 'dir';
        this.USERVAR_ITEM_COUNT = 'cnt';

        this.MMOITEMVAR_OPEN = 'open';

        this.AVATAR_DIRECTIONS = ['E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'NE'];
        this.AVATAR_SPEED = 100; // Expressed in pixels/sec

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

        this.leaveGameButton = view.querySelector('#leaveGameButton');
        this.messageInput = view.querySelector('#messageInput');
        this.chatForm = view.querySelector('#chatForm');
        this.curtainRange = view.querySelector('#curtainRange');

        this.stage = null;
        this.container = null;
        this.curtain = null;
        this.mapObjects = []; // Array of all map objects (static and dynamic, including avatars) used for sprites sorting purposes
    }

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

    initialize()
    {
        super.initialize();

        // Add leave game button click listener
        this.boundOnLeaveGameButtonClick = this.onLeaveGameButtonClick.bind(this);
        this.leaveGameButton.addEventListener('click', this.boundOnLeaveGameButtonClick);
        
        // Add chat form submit listener
        this.boundOnChatFormSubmit = this.onChatFormSubmit.bind(this);
        this.chatForm.addEventListener('submit', this.boundOnChatFormSubmit);
        
        // Add curtain range listener
        this.boundOnCurtainRangeInput = this.onCurtainRangeInput.bind(this);
        this.curtainRange.addEventListener('input', this.boundOnCurtainRangeInput);

        // Add SFS event listeners
        this.sfs.addEventListener(SFSEvent.PROXIMITY_LIST_UPDATE, this.onProximityListUpdate, this);
		this.sfs.addEventListener(SFSEvent.USER_VARIABLES_UPDATE, this.onUserVariablesUpdate, this);
		this.sfs.addEventListener(SFSEvent.PUBLIC_MESSAGE, this.onPublicMessage, this);
		this.sfs.addEventListener(SFSEvent.MMOITEM_VARIABLES_UPDATE, this.onMMOItemVariablesUpdate, this);

        // Init game map
        this.initMap();

        // Init avatars
        this.initAvatars();

        // Show view
        this.showView();
    }

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

        // Remove listeners
        this.leaveGameButton.removeEventListener('click', this.boundOnLeaveGameButtonClick);
        this.chatForm.removeEventListener('submit', this.boundOnChatFormSubmit);
        this.curtainRange.removeEventListener('input', this.boundOnCurtainRangeInput);

        // Clear stage
        this.stage.removeAllChildren();
        this.stage.enableMouseOver(0);
        this.stage.removeAllEventListeners();
        createjs.Ticker.removeAllEventListeners();
    }

    removeSmartFoxListeners()
    {
        if (this.sfs != null)
        {
            this.sfs.removeEventListener(SFSEvent.PROXIMITY_LIST_UPDATE, this.onProximityListUpdate);
            this.sfs.removeEventListener(SFSEvent.USER_VARIABLES_UPDATE, this.onUserVariablesUpdate);
            this.sfs.removeEventListener(SFSEvent.PUBLIC_MESSAGE, this.onPublicMessage);
            this.sfs.removeEventListener(SFSEvent.MMOITEM_VARIABLES_UPDATE, this.onMMOItemVariablesUpdate);
        }
    }

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

    initMap()
    {
        const room = this.sfs.lastJoinedRoom;

        // Setup stage
        this.stage = new createjs.Stage('map');
        this.stage.canvas.width = 1400;
		this.stage.canvas.height = 740;

        // Retrieve Room Variable containing hitmap bytes to be converted to a bitmap image
        const mapData = room.getVariable('mapData').value;
        const hitmap = this.decodeArrayBuffer(mapData.get('hitmap'));

        // Display background
        // The hit area is set to define the non-clickable areas
        const bg = new createjs.Bitmap(AssetManager.bgLoader.getResult(room.name));
        bg.name = 'background';
        bg.hitArea = new createjs.Bitmap(hitmap);
        bg.addEventListener('click', (e) => { this.onMapClick(e); });
        this.stage.addChild(bg);

        // Create container for avatars and other map objects
        this.container = new createjs.Container();
        this.stage.addChild(this.container);

        // Retrieve list of static map items (trees and bushes)
        const mapItems = AssetManager.getStaticMapItems(room.id);

        // Add static map items to the main container
        for (let itemId in mapItems)
        {
            for (let i = 0; i < mapItems[itemId].coordinates.length; i++)
            {
                const coords = mapItems[itemId].coordinates[i];
                const sprite = mapItems[itemId].sprite.clone();

                sprite.x = coords[0];
                sprite.y = coords[1];

                this.container.addChild(sprite);
                this.mapObjects.push(sprite);
            }
        }

        // Add ticker to update the stage
        createjs.Ticker.addEventListener('tick', this.stage);
    }

    initAvatars()
    {
        // Draw curtain
		this.curtain = new createjs.Shape();
        this.stage.addChild(this.curtain);

		// Retrieve Room Variable containing access points coordinates
		// (see Extension comments to understand how data is organized)
		const mapData = this.sfs.lastJoinedRoom.getVariable('mapData').value;
		const accessPoints = mapData.get('accessPoints');

		// Select a random access point among those available
		const index = Math.floor(Math.random() * accessPoints.length / 2) * 2;
		const accessX = accessPoints[index];
		const accessY = accessPoints[index + 1];

		// Set starting direction to a default value
		const dir = this.AVATAR_DIRECTIONS[2];

		// Create current user's avatar
		this.createAvatar(this.sfs.mySelf, accessX, accessY, dir);

		// Arrange map objects sorting
		this.arrangeMapObjects();

		// The position is saved in the User Variables, so that changing it later will trigger the avatar animation
		this.setAvatarVariables(accessX, accessY, dir);

		// Declare current user's position in the MMORoom, to get the proximity list of users
		this.updateServerPosition();
    }

	/**
	 * Converts an image received as bytes to an actual Image.
	 */
	decodeArrayBuffer(buffer)
    {
	    var mime;
	    var a = new Uint8Array(buffer);
	    var nb = a.length;
	    if (nb < 4)
	        return null;
	    var b0 = a[0];
	    var b1 = a[1];
	    var b2 = a[2];
	    var b3 = a[3];
	    if (b0 == 0x89 && b1 == 0x50 && b2 == 0x4E && b3 == 0x47)
	        mime = 'image/png';
	    else if (b0 == 0xff && b1 == 0xd8)
	        mime = 'image/jpeg';
	    else if (b0 == 0x47 && b1 == 0x49 && b2 == 0x46)
	        mime = 'image/gif';
	    else
	        return null;
	    var binary = "";
	    for (var i = 0; i < nb; i++)
	        binary += String.fromCharCode(a[i]);
	    var base64 = window.btoa(binary);
	    var image = new Image();
	    image.src = 'data:' + mime + ';base64,' + base64;
	    return image;
	}

	/**
	 * Returns a reference to an avatar from the user id of its owner.
	 */
	getAvatar(userId)
	{
		return this.container.getChildByName(userId);
	}

	/**
	 * Creates the avatar of a user.
	 */
	createAvatar(user, x, y, dir)
	{
		// Instantiate avatar
		const avatar = new Avatar(user.id, user.name, AssetManager.avatarSpriteSheet, AssetManager.balloonSpriteSheet);

		// Set avatar position and initialize "next" coordinates too (see moveAvatar method)
		avatar.x = avatar.nextX = x;
		avatar.y = avatar.nextY = y;

		// Set initial avatar graphics
		avatar.setGraphics('stand', dir);

		// Show collected barrels counter
		if (user.containsVariable(this.USERVAR_ITEM_COUNT))
			avatar.setBarrelsCounter(user.getVariable(this.USERVAR_ITEM_COUNT).value);

		// Show viewport and Area of Interest boundaries
		if (user.isItMe)
		{
			const room = this.sfs.lastJoinedRoom;
			avatar.setAOIFrame(room.defaultAOI.px * 2, room.defaultAOI.py * 2);
		}

		// Add avatar to container
		this.container.addChild(avatar);

		// Add avatar to array used for sorting
		this.mapObjects.push(avatar);

		// Draw curtain masking area outside of vieport
		if (user.isItMe)
			this.drawCurtain();
	}

	/**
	 * Destroys an avatar.
	 */
	removeAvatar(avatar)
	{
		// Remove tween
        createjs.Tween.removeTweens(avatar);

		// Remove avatar from container
		this.container.removeChild(avatar);

		// Destroy avatar
		avatar.destroy();

		// Remove avatar from array used for sorting purposes
		const index = this.mapObjects.indexOf(avatar);
		this.mapObjects.splice(index, 1);
	}

	/**
	 * Saves the avatar position and direction in the User Variables.
	 */
	setAvatarVariables(px, py, dir)
	{
		// Round the position
		px = Math.round(px);
		py = Math.round(py);
		
		const userVars = [];
		userVars.push(new SFSUserVariable(this.USERVAR_X, px, VariableType.INT));
		userVars.push(new SFSUserVariable(this.USERVAR_Y, py, VariableType.INT));

		if (dir != null)
			userVars.push(new SFSUserVariable(this.USERVAR_DIR, dir, VariableType.STRING));

		this.sfs.send(new SetUserVariablesRequest(userVars));
	}

	/**
	 * Updates the current user's position in the MMORoom, so that all users affected
	 * by the avatar movement receive a PROXIMITY_LIST_UPDATE event.
	 */
	updateServerPosition()
	{
		const myAvatar = this.getAvatar(this.sfs.mySelf.id);

		// Save the coordinates corresponding to the saved position, to be checked during the next tween update
		myAvatar.lastUpdateX = Math.round(myAvatar.x);
		myAvatar.lastUpdateY = Math.round(myAvatar.y);

		const pos = new Vec3D(myAvatar.lastUpdateX, myAvatar.lastUpdateY, 0);
		this.sfs.send(new SetUserPositionRequest(pos));
	}

	/**
	 * Hides the map outside the viewport.
	 */
	drawCurtain()
	{
        const avatar = this.getAvatar(this.sfs.mySelf.id);
		const curtainAlpha = this.curtainRange.value / 100;

		if (avatar != null && curtainAlpha > 0)
		{
            const room = this.sfs.lastJoinedRoom;
			const vpBounds = avatar.getViewportBounds();
			const g = this.curtain.graphics;
			
			g.clear();

            g.beginFill(`rgba(238,238,238,${curtainAlpha})`);

			g.lineTo(room.higherMapLimit.px, 0);
			g.lineTo(room.higherMapLimit.px, room.higherMapLimit.py);
			g.lineTo(0, room.higherMapLimit.py);
			g.lineTo(avatar.x + vpBounds.x, avatar.y + vpBounds.y + vpBounds.height);
			g.lineTo(avatar.x + vpBounds.x + vpBounds.width, avatar.y + vpBounds.y + vpBounds.height);
			g.lineTo(avatar.x + vpBounds.x + vpBounds.width, avatar.y + vpBounds.y);
			g.lineTo(avatar.x + vpBounds.x, avatar.y + vpBounds.y);
			g.lineTo(avatar.x + vpBounds.x, avatar.y + vpBounds.y + vpBounds.height);
			g.lineTo(0, room.higherMapLimit.py);
			g.lineTo(0, 0);

			g.endFill();
		}
	}

	/**
	 * Creates the tween to animate the avatar from the current postion to the destination coordinates saved in the User Variables.
	 * During the movement we have to update the current user's position in the MMORoom so that his proximity list is updated accordingly.
	 */
    moveAvatar(user)
    {
        const avatar = this.getAvatar(user.id);

		// Stop previous animation if any
        createjs.Tween.removeTweens(avatar);

		// Retrieve User Variables
		const px = user.getVariable(this.USERVAR_X).value;
		const py = user.getVariable(this.USERVAR_Y).value;
		const dir = user.getVariable(this.USERVAR_DIR).value;

        // Make avatar play walking animation
		avatar.setGraphics('walk', dir);

		// Calculate animation duration
		// (we want the avatar to move at a constant speed)
        const dx = Math.round(px - avatar.x);
		const dy = Math.round(py - avatar.y);
		const dist = Math.round(Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)));
        const duration = dist / this.AVATAR_SPEED * 1000;

		// Move the avatar to the new position
		// We use the "next" custom properties so we can validate the coordinates during the movement before actually setting it
		// In this way we can make the avatar stop if it crosses a non-walkable area
        const tween = createjs.Tween.get(avatar);
        tween.addEventListener('change', (e) => { this.onAvatarTweenUpdate(e); });
        tween.addEventListener('complete', (e) => { this.onAvatarTweenComplete(e); });
        tween.to({
            nextX: px,
            nextY: py,
        }, duration);
    }

	/**
	 * Updates the current user's position in the MMORoom.
	 * For the current user's avatar, if it steps on a non-walkable area, the movement is stopped.
	 * This method also updates the z-sorting of all avatars at each animation step and redraws the curtain.
	 */
    onAvatarTweenUpdate(e)
    {
        const avatar = e.currentTarget.target;
        
        if (avatar.name == this.sfs.mySelf.id)
		{
            // Check if the next position of the current user's avatar is on a non-walkable area and in case stop it
			if (this.isNonWalkable(avatar.nextX, avatar.nextY))
			{
				// Stop current animation
                createjs.Tween.removeTweens(avatar);

                // Set position to the current coordinates, so all clients will make the avatar reach the same position
				// The previous direction is used
				this.setAvatarVariables(avatar.x, avatar.y);
			}
			else
			{
				// Update avatar position to the "next" coordinates set by the tween
				avatar.x = avatar.nextX;
				avatar.y = avatar.nextY;

				// Save the user posion in the MMORoom
				this.updateServerPosition();
			}
		}
		else
		{
			// Always update coordinates for avatar of other users
			avatar.x = avatar.nextX;
			avatar.y = avatar.nextY;
		}

		// Arrange map objects sorting
		this.arrangeMapObjects();

		// Update viewport curtain
		this.drawCurtain();
    }

	/**
	 * Makes a walking avatar show the standing animation on movement completion.
	 * Also updates the current user's position in the MMORoom.
	 */
    onAvatarTweenComplete(e)
    {
        const avatar = e.currentTarget.target;
        const user = this.sfs.userManager.getUserById(avatar.name);
        const dir = user.getVariable(this.USERVAR_DIR).value;

        avatar.setGraphics('stand', dir);

		if (avatar.name == this.sfs.mySelf.id)
			this.updateServerPosition();
        
    }

	/**
	 * Checks if the passed coordinates hit the non-walkable area of the map.
	 * The hit area of the map objects' container is used.
	 */
	isNonWalkable(px, py)
	{
		return !this.stage.getChildByName('background').hitArea.hitTest(px, py);
	}

	/**
	 * Rearranges the avatars, trees and barrels depths in their container (z-sorting).
	 *
	 * NOTE: as this would be out of the scope for this example, we didn't put too much effort
	 * in the sorting method used to rearrange the avatar depths. In fact this method is not
	 * particularly efficient, because at each animation step all avatars, trees and barrels are rearranged.
	 */
	arrangeMapObjects()
	{
		// Sort avatars based on their y coordinate
		this.mapObjects.sort((a,b) => {
            if (a.y < b.y)
                return -1;
            if (a.y > b.y)
                return 1;

            return 0;
        });

		let i = this.mapObjects.length;

		while (i--)
		{
			if (this.container.getChildIndex(this.mapObjects[i]) != i)
				this.container.setChildIndex(this.mapObjects[i], i);
		}
	}

	// We have to prefix the item id because we need to set the names of avatar and item sprites
	// (so that they can be retrieved with this.container.getChildByName()), but their ids could clash
	getItemNameFromId(itemId)
	{
		return 'item-' + itemId;
	}

	getItemIdFromName(itemName)
	{
		return Number(itemName.substr(5));
	}

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

    onLeaveGameButtonClick(e)
	{
		// Leave current game room
        this.sfs.send(new LeaveRoomRequest());

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

    onCurtainRangeInput(e)
    {
        this.drawCurtain();
    }

    /**
	 * Main map clic handler.
	 * Destination coordinates are saved as SmartFoxServer User Variables.
	 * Also, the direction faced by the avatar is calculated and saved as User Variable.
	 *
	 * NOTE: the avatar of the current user starts moving once the USER_VARIABLES_UPDATE event
	 * is received, just like on the other clients. This can cause a delay in case of network lag.
	 */
    onMapClick(e)
    {
        // Retrieve destination coordinates
		const destX = e.stageX;
		const destY = e.stageY;

		// Get current coordinates
		const myAvatar = this.getAvatar(this.sfs.mySelf.id);
		const currX = myAvatar.x;
		const currY = myAvatar.y;

		// Evaluate avatar movement direction
		const dx = destX - currX;
		const dy = destY - currY;

		const angle = Math.atan(dy / dx);

		let deg = Math.round(angle * 180 / Math.PI);
		if (dx < 0)
			deg += 180;
		else if (dx >= 0 && dy < 0)
			deg += 360;

		let dirIndex = Math.round(deg / 45);
		if (dirIndex >= this.AVATAR_DIRECTIONS.length)
			dirIndex -= this.AVATAR_DIRECTIONS.length;

		const dir = this.AVATAR_DIRECTIONS[dirIndex];

		// Save destination coordinates and direction in User Variables
		this.setAvatarVariables(destX, destY, dir);
    }

    /**
	 * Sends a public message to be displayed avatar's chat bubble of the current user.
	 */
    onChatFormSubmit(e)
    {
        e.preventDefault();
        
        if (this.messageInput.value != '')
        {
            // Send message
            this.sfs.send(new PublicMessageRequest(this.messageInput.value));

            // Reset message input
            this.messageInput.value = '';
            this.messageInput.focus();
        }
    }

	/**
	 * Sends a request to the Room Extension when one of the interactive items on the map (barrels)
	 * is clicked. This makes the server "open" the item and add it to the player's counter.
	 */
	onMMOItemClick(e)
	{
		// Stop event propagation, so that the avatar doesn't start moving
		e.stopPropagation();
		
		// Send the item id to the Extension
		const params = new SFSObject();
		params.putInt('id', this.getItemIdFromName(e.currentTarget.name));

		this.sfs.send(new ExtensionRequest('barrelClick', params, this.sfs.lastJoinedRoom));
	}

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

	/**
	 * Called whenever a proximity list change occurs.
	 * This can be caused by the current user's avatar movement or other users entering/leaving
	 * the current user Area o Interest (AoI).
	 *
	 * The addedUsers list contains the users that entered the current user's AoI since the last update.
	 * Each user has a property declaring its entry position.
	 * Similarly the addedItems list.
	 */
	onProximityListUpdate(evtParams)
	{
		// Loop the removedUsers list in the event params to remove the avatars no more visible
		const removedUsers = evtParams[EventParam.RemovedUsers];
		for (let ru of removedUsers)
		{
			const ra = this.getAvatar(ru.id);

			if (ra != null)
				this.removeAvatar(ra);
		}

		// Loop the addedUsers list in the event params to display the avatars now visible
		const addedUsers = evtParams[EventParam.AddedUsers];
		for (let au of addedUsers)
		{
			// Get user entry point
			const vec = au.aoiEntryPoint;

			// Get avatar direction from User Variables
			const dir = au.getVariable(this.USERVAR_DIR).value;

			// Create avatar
			this.createAvatar(au, vec.px, vec.py, dir);

			// Make the avatar move towards the coordinates set in the User Variables if they are different from the entry point
			if (au.containsVariable(this.USERVAR_X) && au.containsVariable(this.USERVAR_Y))
			{
				const px = au.getVariable(this.USERVAR_X).value;
				const py = au.getVariable(this.USERVAR_Y).value;

				if (px != vec.px || py != vec.py)
					this.moveAvatar(au);
			}
		}

		// Loop the removedItems list in the event params to remove the dynamic items no more visible
		const removedItems = evtParams[EventParam.RemovedItems];
		for (let ri of removedItems)
		{
			const itemName = this.getItemNameFromId(ri.id);
			const sprite = this.container.getChildByName(itemName);

			if (sprite != null)
			{
				// Remove click listener
				sprite.removeAllEventListeners();

				// Remove item from container
				this.container.removeChild(sprite);

				// Remove item from array used for sorting purposes
				const index = this.mapObjects.indexOf(sprite);
				this.mapObjects.splice(index, 1);
			}
		}

		// Loop the addedItems list in the event params to display the dynamic items now visible
		const addedItems = evtParams[EventParam.AddedItems];
		for (let ai of addedItems)
		{
			// Get item entry point
			const vec = ai.aoiEntryPoint;

			// Get opened/closed status from MMOItem Variables
			const isOpen = ai.getVariable(this.MMOITEMVAR_OPEN).value;

			// Create item sprite
			const sprite = new createjs.Sprite(AssetManager.itemSpriteSheet);
			sprite.x = vec.px;
			sprite.y = vec.py;
			sprite.name = this.getItemNameFromId(ai.id);
			sprite.gotoAndStop(isOpen ? 1 : 0);

			// Add click listener if item is still "closed"
	        if (!isOpen)
				sprite.addEventListener('click', (e) => { this.onMMOItemClick(e); });

			this.container.addChild(sprite);
			this.mapObjects.push(sprite);
		}

		// Arrange map objects sorting
		this.arrangeMapObjects();
	}

	/**
	 * Makes an avatar move as soon as its position (saved in User Variables) changes.
	 */
	onUserVariablesUpdate(evtParams)
	{
		const changedVars = evtParams[EventParam.ChangedVars];
		const user = evtParams[EventParam.User];
		const avatar = this.getAvatar(user.id);

		// Check if avatar exists
		if (avatar != null)
		{
			// Check if the user changed their position
			if (changedVars.indexOf(this.USERVAR_X) != -1 || changedVars.indexOf(this.USERVAR_Y) != -1)
			{
				// Move the user avatar
				this.moveAvatar(user);
			}

			// Check if clicked items count for the user was updated
			if (changedVars.indexOf(this.USERVAR_ITEM_COUNT) != -1)
			{
				// Update the user's collected barrels counter
				avatar.setBarrelsCounter(user.getVariable(this.USERVAR_ITEM_COUNT).value);
			}
		}
	}

	/**
	 * Shows the chat message in a bubble on the avatar of the sender.
	 */
	onPublicMessage(evtParams)
	{
		const sender = evtParams[EventParam.Sender];
		const message = evtParams[EventParam.Message];

		const avatar = this.getAvatar(sender.id);

		// Check if avatar exists
		if (avatar != null)
		{
			// Show the chat message
			avatar.showChatMessage(message);
		}
	}

	/**
	 * Makes an item show the "opened" status after it was clicked.
	 */
	onMMOItemVariablesUpdate(evtParams)
	{
		const changedVars = evtParams[EventParam.ChangedVars];
		const item = evtParams[EventParam.MMOItem];

		if (changedVars.indexOf(this.MMOITEMVAR_OPEN) != -1)
		{
			const itemName = this.getItemNameFromId(item.id);
			const sprite = this.container.getChildByName(itemName);

			if (sprite != null)
			{
				sprite.gotoAndStop(1);
				sprite.removeAllEventListeners();
			}
		}
	}
}