/*
 * Game Develop JS Platform
 * Copyright 2013-2014 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
 * This project is released under the GNU Lesser General Public License.
 */

/**
 * Represents a game being played.
 *
 * @constructor
 * @namespace gdjs
 * @class RuntimeGame
 * @param data The object ( usually stored in data.json ) containing the full project data
 * @param spec Optional object for specifiying additional options: {forceFullscreen: ...}
 */
gdjs.RuntimeGame = function(data, spec)
{
    //Safety check: Do gdjs initialization if not already done
    if ( gdjs.objectsTypes.keys.length === 0)
        gdjs.registerObjects();
    if ( gdjs.automatismsTypes.keys.length === 0)
        gdjs.registerAutomatisms();
    if ( gdjs.callbacksRuntimeSceneLoaded.length === 0 &&
         gdjs.callbacksRuntimeSceneUnloaded.length === 0 &&
         gdjs.callbacksObjectDeletedFromScene.length === 0)
        gdjs.registerGlobalCallbacks();

    this._variables = new gdjs.VariablesContainer(data.Variables);
    this._data = data;
    this._imageManager = new gdjs.ImageManager(this);
    this._minFPS = data ? parseInt(data.Info.FPSmin.attr.value, 10) : 15;

    //Game loop management ( see startStandardGameLoop method)
    this._notifySceneForResize = false; //When set to true, the current scene is notified that canvas size changed.

    //Rendering (see createStandardCanvas method)
    this._isFullscreen = false; //Used to track if the canvas is displayed as fullscreen (see setFullscreen method).
    this._forceFullscreen = spec.forceFullscreen || false; //If set to true, the canvas will always be displayed as fullscreen, even if _isFullscreen == false.
    this._renderer = null;
    this._canvasArea = null;
    this._defaultWidth = parseInt(gdjs.projectData.Project.Info.WindowW.attr.value, 10); //Default size for scenes cameras
    this._defaultHeight = parseInt(gdjs.projectData.Project.Info.WindowH.attr.value, 10);
    this._currentWidth = parseInt(gdjs.projectData.Project.Info.WindowW.attr.value, 10); //Current size of the canvas
    this._currentHeight = parseInt(gdjs.projectData.Project.Info.WindowH.attr.value, 10);

    if (navigator.isCocoonJS && !this._forceFullscreen) {
        this._forceFullscreen = true;
        console.log("Forcing fullscreen for CocoonJS.");
    }
    if ( typeof intel != "undefined" ) {
        this._forceFullscreen = true;
        console.log("Forcing fullscreen for Intel XDK.");
    }

    //Inputs :
    this._pressedKeys = new Hashtable();
    this._pressedMouseButtons = new Array(5);
    this._mouseX = 0;
    this._mouseY = 0;
    this._mouseWheelDelta = 0;
};

/**
 * Get the variables of the runtimeGame.
 * @method getVariables
 * @return a variablesContainer object.
 */
gdjs.RuntimeGame.prototype.getVariables = function() {
	return this._variables;
};

gdjs.RuntimeGame.prototype.getImageManager = function() {
	return this._imageManager;
};

/**
 * Get the object containing the game data
 * @method getGameData
 * @return The object associated to the game.
 */
gdjs.RuntimeGame.prototype.getGameData = function() {
	return this._data;
};

/**
 * Get the data associated to a scene.
 *
 * @method getSceneData
 * @param sceneName The name of the scene. If not defined, the first scene will be returned.
 * @return The data associated to the scene.
 */
gdjs.RuntimeGame.prototype.getSceneData = function(sceneName) {
	var scene = undefined;
	gdjs.iterateOver(this._data.Scenes, "Scene", function(sceneData) {
		if ( sceneName === undefined || sceneData.attr.nom === sceneName ) {
			scene = sceneData;
			return false;
		}
	});

	if ( scene === undefined )
		console.warn("The game has no scene called \""+sceneName+"\"");

	return scene;
};

/**
 * Check if a scene exists
 *
 * @method hasScene
 * @param sceneName The name of the scene to search.
 * @return true if the scene exists. If sceneName is undefined, true if the game has a scene.
 */
gdjs.RuntimeGame.prototype.hasScene = function(sceneName) {
	var isTrue = false;
	gdjs.iterateOver(this._data.Scenes, "Scene", function(sceneData) {
		if ( sceneName === undefined || sceneData.attr.nom == sceneName ) {
			isTrue = true;
			return false;
		}
	});

	return isTrue;
};

/**
 * Get the data associated to an external layout.
 *
 * @method getExternalLayoutData
 * @param name The name of the external layout.
 * @return The data associated to the external layout or null if not found.
 */
gdjs.RuntimeGame.prototype.getExternalLayoutData = function(name) {
    var externalLayout = null;
    gdjs.iterateOver(this._data.ExternalLayouts, "ExternalLayout", function(layoutData) {
        if ( layoutData.attr.Name === name ) {
            externalLayout = layoutData;
            return false;
        }
    });

    return externalLayout;
};

/**
 * Get the data representing all the initial objects of the game.
 * @method getInitialObjectsData
 * @return The data associated to the initial objects.
 */
gdjs.RuntimeGame.prototype.getInitialObjectsData = function() {
	return this._data.Objects;
};

/**
 * Should be called whenever a key is pressed
 * @method onKeyPressed
 * @param keyCode {Number} The key code associated to the key press.
 */
gdjs.RuntimeGame.prototype.onKeyPressed = function(keyCode) {
	this._pressedKeys.put(keyCode, true);
};

/**
 * Should be called whenever a key is released
 * @method onKeyReleased
 * @param keyCode {Number} The key code associated to the key release.
 */
gdjs.RuntimeGame.prototype.onKeyReleased = function(keyCode) {
	this._pressedKeys.put(keyCode, false);
};

/**
 * Return true if the key corresponding to keyCode is pressed.
 * @method isKeyPressed
 * @param keyCode {Number} The key code to be tested.
 */
gdjs.RuntimeGame.prototype.isKeyPressed = function(keyCode) {
	return this._pressedKeys.containsKey(keyCode) && this._pressedKeys.get(keyCode);
};

/**
 * Return true if any key is pressed
 * @method anyKeyPressed
 */
gdjs.RuntimeGame.prototype.anyKeyPressed = function(keyCode) {
	var allKeys = this._pressedKeys.entries();

	for(var i = 0, len = allKeys.length;i<len;++i) {
		if (allKeys[i][1]) {
			return true;
		}
	}

	return false;
};

/**
 * Should be called when the mouse is moved.<br>
 * Please note that the coordinates must be expressed relative to the view position.
 *
 * @method onMouseMove
 * @param x {Number} The mouse new X position
 * @param y {Number} The mouse new Y position
 */
gdjs.RuntimeGame.prototype.onMouseMove = function(x,y) {
	this._mouseX = x;
	this._mouseY = y;
};

/**
 * Get the mouse X position
 *
 * @method getMouseX
 * @return the mouse X position, relative to the game view.
 */
gdjs.RuntimeGame.prototype.getMouseX = function() {
	return this._mouseX;
};

/**
 * Get the mouse Y position
 *
 * @method getMouseY
 * @return the mouse Y position, relative to the game view.
 */
gdjs.RuntimeGame.prototype.getMouseY = function() {
	return this._mouseY;
};

gdjs.RuntimeGame.prototype.getDefaultWidth = function() {
    return this._defaultWidth;
};

gdjs.RuntimeGame.prototype.getDefaultHeight = function() {
    return this._defaultHeight;
};

/**
 * Change the default width of the game: It won't affect the canvas size, but
 * new scene cameras will be created with this size.
 * @method setDefaultWidth
 * @param width {Number} The new default width
 */
gdjs.RuntimeGame.prototype.setDefaultWidth = function(width) {
    this._defaultWidth = width;
};

/**
 * Change the default height of the game: It won't affect the canvas size, but
 * new scene cameras will be created with this size.
 * @method setDefaultHeight
 * @param height {Number} The new default height
 */
gdjs.RuntimeGame.prototype.setDefaultHeight = function(height) {
    this._defaultHeight = height;
};

/**
 * Change the size of the canvas displaying the game.
 * Note that if the canvas is fullscreen, it won't be resized, but when going back to
 * non fullscreen mode, the requested size will be used.
 *
 * @method setCanvasSize
 * @param width {Number} The new width
 * @param height {Number} The new height
 */
gdjs.RuntimeGame.prototype.setCanvasSize = function(width, height) {
    this._currentWidth = width;
    this._currentHeight = height;

    this._resizeCanvas(this._renderer, this._canvasArea, this._isFullscreen,
        this._currentWidth, this._currentHeight);
    this._notifySceneForResize = true;
};

/**
 * Should be called whenever a mouse button is pressed
 * @method onMouseButtonPressed
 * @param buttonCode {Number} The mouse button code associated to the event.<br>0: Left button<br>1: Right button
 */
gdjs.RuntimeGame.prototype.onMouseButtonPressed = function(buttonCode) {
	this._pressedMouseButtons[buttonCode] = true;
};

/**
 * Should be called whenever a mouse button is released
 * @method onMouseButtonReleased
 * @param buttonCode {Number} The mouse button code associated to the event. ( See onMouseButtonPressed )
 */
gdjs.RuntimeGame.prototype.onMouseButtonReleased = function(buttonCode) {
	this._pressedMouseButtons[buttonCode] = false;
};

/**
 * Return true if the mouse button corresponding to buttonCode is pressed.
 * @method isMouseButtonPressed
 * @param buttonCode {Number} The mouse button code.<br>0: Left button<br>1: Right button
 */
gdjs.RuntimeGame.prototype.isMouseButtonPressed = function(buttonCode) {
	return this._pressedMouseButtons[buttonCode] !== undefined && this._pressedMouseButtons[buttonCode];
};

/**
 * Should be called whenever the mouse wheel is used
 * @method onMouseWheel
 * @param wheelDelta {Number} The mouse wheel delta
 */
gdjs.RuntimeGame.prototype.onMouseWheel = function(wheelDelta) {
	this._mouseWheelDelta = wheelDelta;
};

/**
 * Return the mouse wheel delta
 * @method getMouseWheelDelta
 */
gdjs.RuntimeGame.prototype.getMouseWheelDelta = function() {
	return this._mouseWheelDelta;
};

/**
 * Return the minimal fps that must be guaranteed by the game.
 * ( Otherwise, game is slowed down ).
 * @method getMinimalFramerate
 */
gdjs.RuntimeGame.prototype.getMinimalFramerate = function() {
	return this._minFPS;
};

/**
 * Add the standard events handler.
 * Be sure that the game has a renderer (See createStandardRenderer).
 * @method bindStandardEvents
 */
gdjs.RuntimeGame.prototype.bindStandardEvents = function(window, document) {
    if (!this._renderer || !this._canvasArea) {
        console.log("Unable to bind events to the game! Be sure that there is a renderer and a canvas area associated to the game.");
        return;
    }

    var isMSIE = /*@cc_on!@*/0;
    this._renderer.view.style.cssText="idtkscale:'ScaleAspectFill';"; //CocoonJS support

    var game = this;
    document.onkeydown = function(e) {
        game.onKeyPressed(e.keyCode);
    };
    document.onkeyup = function(e) {
        game.onKeyReleased(e.keyCode);
    };
    this._renderer.view.onmousemove = function(e){
        var pos = [0,0];
        if (e.pageX) {
            pos[0] = e.pageX-game._canvasArea.offsetLeft;
            pos[1] = e.pageY-game._canvasArea.offsetTop;
        }
        else {
            pos[0] = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft-game._canvasArea.offsetLeft;
            pos[1] = e.clientY + document.body.scrollTop + document.documentElement.scrollTop-game._canvasArea.offsetTop;
        }

        //Handle the fact that the game is stretched to fill the canvas.
        pos[0] *= game.getDefaultWidth()/game._renderer.view.width;
        pos[1] *= game.getDefaultHeight()/game._renderer.view.height;

        game.onMouseMove(pos[0], pos[1]);
    };
    this._renderer.view.onmousedown = function(e){
        game.onMouseButtonPressed(e.button === 2 ? 1 : 0);
        if (window.focus !== undefined) window.focus();
        return false;
    };
    this._renderer.view.onmouseup = function(e){
        game.onMouseButtonReleased(e.button === 2 ? 1 : 0);
        return false;
    };
    this._renderer.view.onmouseout = function(e){
        game.onMouseButtonReleased(0);
        game.onMouseButtonReleased(1);
        game.onMouseWheel(0);
        return false;
    };
    window.addEventListener('click', function(e) {
        if (window.focus !== undefined) window.focus();
        e.preventDefault();
        return false;
    }, false);
    this._renderer.view.oncontextmenu = function(event) {
        event.preventDefault();
        event.stopPropagation();
        return false;
    };
    this._renderer.view.onmousewheel = function (event){
        game.onMouseWheel(event.wheelDelta);
    };
    //Simulate mouse events when receiving touch events
    window.addEventListener('touchmove', function(e){
        e.preventDefault();
        if ( e.touches && e.touches.length > 0 ) {
            var pos = [0,0];
            if (e.touches[0].pageX) {
                pos[0] = e.touches[0].pageX-game._canvasArea.offsetLeft;
                pos[1] = e.touches[0].pageY-game._canvasArea.offsetTop;
            }
            else {
                pos[0] = e.touches[0].clientX + document.body.scrollLeft + document.documentElement.scrollLeft-game._canvasArea.offsetLeft;
                pos[1] = e.touches[0].clientY + document.body.scrollTop + document.documentElement.scrollTop-game._canvasArea.offsetTop;
            }

            //Handle the fact that the game is stretched to fill the canvas.
            pos[0] *= game.getDefaultWidth()/game._renderer.view.width;
            pos[1] *= game.getDefaultHeight()/game._renderer.view.height;

            game.onMouseMove(pos[0], pos[1]);
        }
    });
    window.addEventListener('touchstart', function(e){
        e.preventDefault();
        if ( e.touches && e.touches.length > 0 ) {
            var pos = [0,0];
            if (e.touches[0].pageX) {
                if ( isNaN(game._canvasArea.offsetLeft) ) {
                    game._canvasArea.offsetLeft = 0;
                    game._canvasArea.offsetTop = 0;
                }

                pos[0] = e.touches[0].pageX-game._canvasArea.offsetLeft;
                pos[1] = e.touches[0].pageY-game._canvasArea.offsetTop;
            }
            else {
                if ( isNaN(document.body.scrollLeft) ) {
                    document.body.scrollLeft = 0;
                    document.body.scrollTop = 0;
                }
                if ( document.documentElement === undefined || document.documentElement === null ) {
                    document.documentElement = {};
                }
                if ( isNaN(document.documentElement.scrollLeft) ) {
                    document.documentElement.scrollLeft = 0;
                    document.documentElement.scrollTop = 0;
                }
                if ( isNaN(game._canvasArea.offsetLeft) ) {
                    game._canvasArea.offsetLeft = 0;
                    game._canvasArea.offsetTop = 0;
                }
                pos[0] = e.touches[0].clientX + document.body.scrollLeft + document.documentElement.scrollLeft-game._canvasArea.offsetLeft;
                pos[1] = e.touches[0].clientY + document.body.scrollTop + document.documentElement.scrollTop-game._canvasArea.offsetTop;
            }

            //Handle the fact that the game is stretched to fill the canvas.
            pos[0] *= game.getDefaultWidth()/game._renderer.view.width;
            pos[1] *= game.getDefaultHeight()/game._renderer.view.height;

            game.onMouseMove(pos[0], pos[1]);
        }
        game.onMouseButtonPressed(0);
        return false;
    });
    window.addEventListener('touchend', function(e){
        e.preventDefault();
        if ( e.touches && e.touches.length > 0 ) {
            var pos = [0,0];
            if (e.touches[0].pageX) {
                pos[0] = e.touches[0].pageX-game._canvasArea.offsetLeft;
                pos[1] = e.touches[0].pageY-game._canvasArea.offsetTop;
            }
            else {
                pos[0] = e.touches[0].clientX + document.body.scrollLeft + document.documentElement.scrollLeft-game._canvasArea.offsetLeft;
                pos[1] = e.touches[0].clientY + document.body.scrollTop + document.documentElement.scrollTop-game._canvasArea.offsetTop;
            }

            //Handle the fact that the game is stretched to fill the canvas.
            pos[0] *= game.getDefaultWidth()/game._renderer.view.width;
            pos[1] *= game.getDefaultHeight()/game._renderer.view.height;

            game.onMouseMove(pos[0], pos[1]);
        }
        game.onMouseButtonReleased(0);
        return false;
    });
    //Hide the adress bar on handheld devices.
    window.addEventListener('load', function(e) {
        setTimeout(function() {
            if ( document.documentElement.clientWidth < 600 ) {
                window.scrollTo(0, 1);
            }
        }, 1);
    }, false);
};

/**
 * De/activate fullscreen for the canvas rendering the game.
 * @method setFullScreen
 */
gdjs.RuntimeGame.prototype.setFullScreen = function(enable) {
    if (this._forceFullscreen) return;

    if (this._isFullscreen !== enable) {
        this._isFullscreen = !!enable;
        this._resizeCanvas(this._renderer, this._canvasArea, this._isFullscreen,
            this._currentWidth, this._currentHeight);
        this._notifySceneForResize = true;

        if (this._isFullscreen) {
            if(document.documentElement.requestFullScreen) {
                document.documentElement.requestFullScreen();
            } else if(document.documentElement.mozRequestFullScreen) {
                document.documentElement.mozRequestFullScreen();
            } else if(document.documentElement.webkitRequestFullScreen) {
                document.documentElement.webkitRequestFullScreen();
            }
        }
        else {
            if(document.cancelFullScreen) {
                document.cancelFullScreen();
            } else if(document.mozCancelFullScreen) {
                document.mozCancelFullScreen();
            } else if(document.webkitCancelFullScreen) {
                document.webkitCancelFullScreen();
            }
        }
    }
};

/**
 * Create a standard canvas inside canvasArea.
 * The game keep a reference to this canvas and will renderer in it.
 *
 * @method createStandardCanvas
 */
gdjs.RuntimeGame.prototype.createStandardCanvas = function(canvasArea) {
    this._canvasArea = canvasArea;

    //Create the renderer and setup the rendering area
    this._renderer = PIXI.autoDetectRenderer(this._defaultWidth, this._defaultHeight);
    canvasArea.style["position"] = "absolute";
    canvasArea.appendChild(this._renderer.view); // add the renderer view element to the DOM
    canvasArea.tabindex="1"; //Ensure that the canvas has the focus.
    canvasArea.style.overflow="hidden"; //No scrollbar in any case.
    this._resizeCanvas(this._renderer, this._canvasArea, this._forceFullscreen || this._isFullscreen,
        this._currentWidth, this._currentHeight);

    //Handle resize
    var game = this;
    window.addEventListener("resize", function() {
        game._resizeCanvas(game._renderer, game._canvasArea, this._forceFullscreen || game._isFullscreen,
            game._currentWidth, game._currentHeight);
        game._notifySceneForResize = true;
    });

    return this._renderer;
};

/**
 * Resize the canvas. Parameters width or height are ignored if isFullscreen === true.
 *
 * @method _resizeCanvas
 * @private
 * @static
 */
gdjs.RuntimeGame.prototype._resizeCanvas = function(renderer, canvasArea, isFullscreen, width, height) {
    if ( isFullscreen ) {
        width = window.innerWidth;
        height = window.innerHeight;
    }

    if (renderer.width !== width || renderer.height !== height) renderer.resize(width, height);
    canvasArea.style["top"] = isFullscreen ? "0" : (gdjs.getDocHeight()-height)/2+"px";
    canvasArea.style["left"] = isFullscreen ? "0" : (gdjs.getDocWidth()-width)/2+"px";
    canvasArea.style.width = width+"px";
    canvasArea.style.height = height+"px";
};

/**
 * Load all assets, displaying progress in renderer.
 * @method loadAllAssets
 */
gdjs.RuntimeGame.prototype.loadAllAssets = function(callback) {

    //Load all assets
    var loadingStage = new PIXI.Stage();
    var text = new PIXI.Text(" ", {font: "bold 60px Arial", fill: "#FFFFFF", align: "center"});
    loadingStage.addChild(text);
    text.position.x = this._renderer.width/2-50;
    text.position.y = this._renderer.height/2;
    var loadingCount = 0;

    var assets = [];
    gdjs.iterateOver(gdjs.projectData.Project.Resources.Resources, "Resource", function(res) {
        if ( res.attr.file ) {
            assets.push(res.attr.file);
        }
    });

    var game = this;
    if ( assets.length !== 0 ) {
        var assetLoader = new PIXI.AssetLoader(assets);
        assetLoader.onComplete = onAssetsLoaded;
        assetLoader.onProgress = onAssetsLoadingProgress;
        assetLoader.load();
    }
    else {
        onAssetsLoaded();
    }

    function onAssetsLoaded() {
        callback();
    }

    function onAssetsLoadingProgress() {
        game._renderer.render(loadingStage);
        loadingCount++;
        text.setText(Math.floor(loadingCount/assets.length*100) + "%");
    }
};

/**
 * Launch the game, displayed in the renderer associated to the game (see createStandardCanvas).<br>
 * The method returns when the game is closed.
 * @method startStandardGameLoop
 */
gdjs.RuntimeGame.prototype.startStandardGameLoop = function() {

    if ( !this.hasScene() ) {
        console.log("The game has no scene.");
        return;
    }

    //Create the scene to be played
    var currentScene = new gdjs.RuntimeScene(this, this._renderer);
    var firstSceneName = gdjs.projectData.Project.Scenes.attr.firstScene;
    var firstsceneData = this.hasScene(firstSceneName) ? this.getSceneData(firstSceneName) : this.getSceneData();

    currentScene.loadFromScene(firstsceneData);

    requestAnimFrame(gameLoop);

    //The standard game loop
    var game = this;
    function gameLoop() {

        //Manage resize events.
        if ( game._notifySceneForResize ) {
            currentScene.onCanvasResized();
            game._notifySceneForResize = false;
        }

        //Render and step the scene.
        if ( !currentScene.renderAndStep() ) {
            //Something special was requested by the current scene.
            if ( currentScene.gameStopRequested() )
                postGameScreen();
            else {
                var nextSceneName = currentScene.getRequestedScene();
                currentScene = new gdjs.RuntimeScene(game, game._renderer);
                currentScene.loadFromScene(game.getSceneData(nextSceneName));
                requestAnimFrame( gameLoop );
            }
        }
        else {
            requestAnimFrame( gameLoop );
        }
    }
};