Source: director.js

goog.provide('lime.Director');


goog.require('lime.CoverNode');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.ViewportSizeMonitor');
goog.require('goog.dom.classes');
goog.require('goog.events');
goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Size');
goog.require('goog.math.Vec2');
goog.require('goog.style');
goog.require('lime');
goog.require('lime.Node');
goog.require('lime.events.EventDispatcher');
goog.require('lime.helper.PauseScene');
goog.require('lime.scheduleManager');
goog.require('lime.transitions.Transition');


/**
 * Director object. Base object for every game.
 * @param {Element} parentElement Parent element for director.
 * @param {number=} opt_width Optionaly define what height and width the director should have.
 * @param {number=} opt_height Optionaly define what height and width the director should have.
 * @constructor
 * @extends lime.Node
 */
lime.Director = function(parentElement, opt_width, opt_height) {
    lime.Node.call(this);

    // Unlike other nodes Director is always in the DOM as
    // it requires parentNode in the constructor
    this.inTree_ = true;

    this.setAnchorPoint(0, 0);
    // todo: maybe easier if director just positions itselt
    // to the center of the screen

    /**
     * Array of Scene instances. Last one is the active scene.
     * @type {Array.<lime.Scene>}
     * @private
     */
    this.sceneStack_ = [];

    /**
     * Array of CoverNode instances.
     * @type {Array.<lime.CoverNode>}
     * @private
     */
    this.coverStack_ = [];

    this.domClassName = goog.getCssName('lime-director');
    this.createDomElement();
    parentElement.appendChild(this.domElement);



    if (goog.userAgent.WEBKIT && goog.userAgent.MOBILE) {
        //todo: Not pretty solution. Cover layers may not be needed at all.
        this.coverElementBelow = document.createElement('div');
        goog.dom.classes.add(this.coverElementBelow,
            goog.getCssName('lime-cover-below'));
        goog.dom.insertSiblingBefore(this.coverElementBelow, this.domElement);

        this.coverElementAbove = document.createElement('div');
        goog.dom.classes.add(this.coverElementAbove,
            goog.getCssName('lime-cover-above'));
        goog.dom.insertSiblingAfter(this.coverElementAbove, this.domElement);
    }

    if (parentElement.style['position'] != 'absolute') {
        parentElement.style['position'] = 'relative';
    }
    parentElement.style['overflow'] = 'hidden';

    if (parentElement == document.body) {
        goog.style.installStyles('html,body{margin:0;padding:0;height:100%;}');

        var meta = document.createElement('meta');
        meta.name = 'viewport';
        var content = 'initial-scale=1.0,minimum-scale=1,' +
            'maximum-scale=1.0,user-scalable=no';
        if ((/android/i).test(navigator.userAgent)) {
            content += ',target-densityDpi=device-dpi';
        }

        meta.content = content;
        document.getElementsByTagName('head').item(0).appendChild(meta);


        //todo: look for a less hacky solution
        if (goog.userAgent.MOBILE && !goog.global['navigator'].standalone) {
            var that = this;
            setTimeout(function() {
                window.scrollTo(0, 0);
                that.invalidateSize_()
            }, 100);
        }
    }

    var width, parentSize = goog.style.getSize(parentElement);

    this.setSize(new goog.math.Size(
        width = arguments[1] || parentSize.width || lime.Director.DEFAULT_WIDTH,
        arguments[2] || parentSize.height * width / parentSize.width || lime.Director.DEFAULT_HEIGHT));

    // --define goog.debug=false
    this.setDisplayFPS(goog.DEBUG);
    this.setPaused(false);


    var vsm = new goog.dom.ViewportSizeMonitor();
    goog.events.listen(vsm, goog.events.EventType.RESIZE,
        this.invalidateSize_, false, this);
    goog.events.listen(goog.global, 'orientationchange',
        this.invalidateSize_, false, this);


    lime.scheduleManager.schedule(this.step_, this);


    this.eventDispatcher = new lime.events.EventDispatcher(this);

    goog.events.listen(this, ['touchmove', 'touchstart'], function(e) {
        e.event.preventDefault();
    }, false, this);

    // todo: check if all those are really neccessary as Event code
    // is much more mature now
    goog.events.listen(this, ['mouseup', 'touchend', 'mouseout', 'touchcancel'], function() {}, false);


    this.invalidateSize_();

    if (goog.DEBUG) {
        goog.events.listen(goog.global, 'keyup', this.keyUpHandler_, false, this);
    }

};
goog.inherits(lime.Director, lime.Node);


/**
 * Milliseconds between recalculating FPS value
 * @const
 * @type {number}
 */
lime.Director.FPS_INTERVAL = 100;

/**
 * Default width of the Director
 * @const
 * @type {number}
 */
lime.Director.DEFAULT_WIDTH = 400;


/**
 * Default height of the Director
 * @const
 * @type {number}
 */
lime.Director.DEFAULT_HEIGHT = 400;


/**
 * Returns true if director is paused? On paused state
 * the update timer doesn't fire.
 * @return {boolean} If director is paused.
 */
lime.Director.prototype.isPaused = function() {
    return this.isPaused_;
};


/**
 * Pauses or resumes the director
 * @param {boolean} value Pause or resume.
 * @return {lime.Director} The director object itself.
 */
lime.Director.prototype.setPaused = function(value) {
    if (this.isPaused_ === value) return this;
    this.isPaused_ = value;
    lime.scheduleManager.changeDirectorActivity(this, !value);
    if (this.isPaused_) {
        var pauseClass = this.pauseClassFactory || lime.helper.PauseScene;
        this.pauseScene = new pauseClass();
        this.pushScene(this.pauseScene);
    } else if (this.pauseScene) {
        this.popScene();
        delete this.pauseScene;
    }
    return this;
};


/**
 * Returns true if FPS counter is displayed
 * @return {boolean} FPS is displayed.
 */
lime.Director.prototype.isDisplayFPS = function() {
    return this.displayFPS_;
};

/**
 * Show or hide FPS counter
 * @param {boolean} value Display FPS?
 * @return {lime.Director} The director object itself.
 */
lime.Director.prototype.setDisplayFPS = function(value) {
    if (this.displayFPS_ && !value) {
        goog.dom.removeNode(this.fpsElement_);
    } else if (!this.displayFPS_ && value) {
        this.frames_ = 0;
        this.accumDt_ = 0;

        this.fpsElement_ = goog.dom.createDom('div');
        goog.dom.classes.add(this.fpsElement_, goog.getCssName('lime-fps'));
        this.domElement.parentNode.appendChild(this.fpsElement_);
    }

    this.displayFPS_ = value;
    return this;
};


/**
 * Get current active scene
 * @return {lime.Scene} Currently active scene.
 */
lime.Director.prototype.getCurrentScene = function() {
    return this.sceneStack_.length ?
        this.sceneStack_[this.sceneStack_.length - 1] : null;
};

/** @inheritDoc */
lime.Director.prototype.getDirector = function() {
    return this;
};

/** @inheritDoc */
lime.Director.prototype.getScene = function() {
    return null;
};

/**
 * Timeline function.
 * @param {number} delta Milliseconds since last step.
 * @private
 */
lime.Director.prototype.step_ = function(delta) {
    if (this.isDisplayFPS()) {

        this.frames_++;
        this.accumDt_ += delta;
        if (this.accumDt_ > lime.Director.FPS_INTERVAL) {
            this.fps = ((1000 * this.frames_) / this.accumDt_);
            goog.dom.setTextContent(this.fpsElement_, this.fps.toFixed(2));
            this.frames_ = 0;
            this.accumDt_ = 0;
        }
    }
    lime.updateDirtyObjects();
};


/**
 * Replace current scene with new scene
 * @param {lime.Scene} scene New scene.
 * @param {function(lime.Scene,lime.Scene,boolean=)=} opt_transition Transition played.
 * @param {number=} opt_duration Duration of transition.
 */
lime.Director.prototype.replaceScene = function(scene, opt_transition,
    opt_duration) {

    scene.setSize(this.getSize().clone());

    var transitionclass = opt_transition || lime.transitions.Transition;

    var outgoing = null;
    if (this.sceneStack_.length)
        outgoing = this.sceneStack_[this.sceneStack_.length - 1];



    var removelist = [];
    var i = this.sceneStack_.length;
    while (--i >= 0) {
        this.sceneStack_[i].wasRemovedFromTree();
        removelist.push(this.sceneStack_[i].domElement);
        this.sceneStack_[i].parent_ = null;
    }
    this.sceneStack_.length = 0;

    this.sceneStack_.push(scene);
    scene.domElement.style['display'] = 'none';
    this.domElement.appendChild(scene.domElement);
    scene.parent_ = this;
    scene.wasAddedToTree();

    var transition = new transitionclass(outgoing, scene);

    goog.events.listenOnce(transition, 'end', function() {
        var i = removelist.length;
        while (--i >= 0) {
            goog.dom.removeNode(removelist[i]);
        }
        removelist.length = 0;

    }, false, this);

    if (goog.isDef(opt_duration)) {
        transition.setDuration(opt_duration);
    }

    transition.start();
    return transition;

};

/** @inheritDoc */
lime.Director.prototype.updateLayout = function() {
    // debugger;
    this.dirty_ &= ~lime.Dirty.LAYOUT;
};

/**
 * Push scene to the top of scene stack
 * @param {lime.Scene} scene New scene.
 * @param {function(lime.Scene,lime.Scene,boolean=)=} opt_transition Transition played.
 * @param {number=} opt_duration Duration of transition.
 * @return Transition object if opt_transition is defined
 */
lime.Director.prototype.pushScene = function(scene, opt_transition, opt_duration) {
    var transition, outgoing;

    scene.setSize(this.getSize().clone());

    if (goog.isDef(opt_transition) && this.sceneStack_.length) {
        outgoing = this.sceneStack_[this.sceneStack_.length - 1];
        transition = new opt_transition(outgoing, scene);

        if (goog.isDef(opt_duration)) {
            transition.setDuration(opt_duration);
        }
        scene.domElement.style['display'] = 'none';
    }
    this.sceneStack_.push(scene);
    this.domElement.appendChild(scene.domElement);
    scene.parent_ = this;
    scene.wasAddedToTree();

    if (transition) {
        transition.start();
        return transition;
    }
};


/**
 * Remove current scene from the stack
 * @param {function(lime.Scene,lime.Scene,boolean=)=} opt_transition Transition played.
 * @param {number=} opt_duration Duration of transition.
 * @return Transition object if opt_transition is defined
 */
lime.Director.prototype.popScene = function(opt_transition, opt_duration) {
    var transition,
        outgoing = this.getCurrentScene();

    if (goog.isNull(outgoing)) return;

    var popOutgoing = function() {
        outgoing.wasRemovedFromTree();
        outgoing.parent_ = null;
        goog.dom.removeNode(outgoing.domElement);
        this.sceneStack_.pop();
        outgoing = null; // GC
    };
    // Transitions require an existing incoming scene
    if (goog.isDef(opt_transition) && (this.sceneStack_.length > 1)) {
        transition = new opt_transition(outgoing, this.sceneStack_[this.sceneStack_.length - 2]);

        if (goog.isDef(opt_duration)) {
            transition.setDuration(opt_duration);
        }
        goog.events.listenOnce(transition, 'end', popOutgoing, false, this);
    } else {
        popOutgoing.call(this);
    }
    if (transition) {
        transition.start();
        return transition;
    }
};


/**
 * Add CoverNode object to the viewport
 * @param {lime.CoverNode} cover Covernode.
 * @param {boolean} opt_addAboveDirector Cover is added above director object.
 */
lime.Director.prototype.addCover = function(cover, opt_addAboveDirector) {
    //mobile safari performes much better with this hack. needs investigation.
    if (goog.userAgent.WEBKIT && goog.userAgent.MOBILE) {
        if (opt_addAboveDirector) {
            this.coverElementAbove.appendChild(cover.domElement);
        } else {
            this.coverElementBelow.appendChild(cover.domElement);
        }

    } else {
        if (opt_addAboveDirector) {
            goog.dom.insertSiblingAfter(cover.domElement, this.domElement);
        } else {
            goog.dom.insertSiblingBefore(cover.domElement, this.domElement);
        }
    }
    cover.director = this;
    this.coverStack_.push(cover);
};

/**
 * Remove CoverNode object from the viewport
 * @param {lime.CoverNode} cover Cover to remove.
 */
lime.Director.prototype.removeCover = function(cover) {
    goog.array.remove(this.coverStack_, cover);
    goog.dom.removeNode(cover.domElement);
};


/**
 * Return bounds of director,
 * @param {goog.math.Box} box Edges.
 * @return {goog.math.Box} new bounds.
 */
lime.Director.prototype.getBounds = function(box) {
    //todo:This should basically be same as boundingbox on lime.node
    var position = this.getPosition(),
        scale = this.getScale();
    return new goog.math.Box(
        box.top - position.y / scale.y,
        box.right - position.x / scale.x,
        box.bottom - position.y / scale.y,
        box.left - position.x / scale.x);
};

/**
 * @inheritDoc
 */
lime.Director.prototype.screenToLocal = function(c) {
    var coord = c.clone();
    coord.x -= this.domOffset.x + this.position_.x;
    coord.y -= this.domOffset.y + this.position_.y;

    coord.x /= this.scale_.x;
    coord.y /= this.scale_.y;
    return coord;
};

/**
 * @inheritDoc
 */
lime.Director.prototype.localToScreen = function(c) {
    var coord = c.clone();
    coord.x *= this.scale_.x;
    coord.y *= this.scale_.y;

    coord.x += this.domOffset.x + this.position_.x;
    coord.y += this.domOffset.y + this.position_.y;

    return coord;
};


/**
 * @inheritDoc
 */
lime.Director.prototype.update = function() {
    lime.Node.prototype.update.call(this);

    var i = this.coverStack_.length;
    while (--i >= 0) {
        this.coverStack_[i].update();
    }
};


/**
 * Update dimensions based on viewport dimension changes
 * @private
 */
lime.Director.prototype.invalidateSize_ = function() {

    var stageSize = goog.style.getSize(this.domElement.parentNode);

    if (this.domElement.parentNode == document.body) {
        window.scrollTo(0, 0);
        if (goog.isNumber(window.innerHeight)) {
            stageSize.height = window.innerHeight;
        }
    }

    var realSize = this.getSize().clone().scaleToFit(stageSize);

    var scale = realSize.width / this.getSize().width;
    this.setScale(scale);

    if (stageSize.aspectRatio() < realSize.aspectRatio()) {
        this.setPosition(0, (stageSize.height - realSize.height) / 2);
    } else {
        this.setPosition((stageSize.width - realSize.width) / 2, 0);
    }

    this.updateDomOffset_();

    // overflow hidden is for hiding away unused edges of document
    // height addition is because scroll(0,0) doesn't work any more if the
    // document has no edge @tonis todo:look for less hacky solution(iframe?).
    if (goog.userAgent.MOBILE && this.domElement.parentNode == document.body) {
        if (this.overflowStyle_) goog.style.uninstallStyles(this.overflowStyle_);
        this.overflowStyle_ = goog.style.installStyles(
            'html{height:' + (stageSize.height + 120) + 'px;overflow:hidden;}');
    }

};

/**
 * Add support for adding game to Springboard as a
 * web application on iOS devices
 */
lime.Director.prototype.makeMobileWebAppCapable = function() {

    var meta = document.createElement('meta');
    meta.name = 'apple-mobile-web-app-capable';
    meta.content = 'yes';
    document.getElementsByTagName('head').item(0).appendChild(meta);

    meta = document.createElement('meta');
    meta.name = 'apple-mobile-web-app-status-bar-style';
    meta.content = 'black';
    document.getElementsByTagName('head').item(0).appendChild(meta);

    var visited = false;
    if (goog.isDef(localStorage)) {
        visited = localStorage.getItem('_lime_visited');
    }

    var ios = (/(ipod|iphone|ipad)/i).test(navigator.userAgent);
    if (ios && !window.navigator.standalone && COMPILED && !visited && this.domElement.parentNode == document.body) {
        alert('Please install this page as a web app by ' +
            'clicking Share + Add to home screen.');
        if (goog.isDef(localStorage)) {
            localStorage.setItem('_lime_visited', true);
        }
    }

};

/**
 * Updates the cached value of directors parentelement position in the viewport
 * @private
 */
lime.Director.prototype.updateDomOffset_ = function() {
    this.domOffset = goog.style.getPageOffset(this.domElement.parentNode);
};

/**
 * @private
 */
lime.Director.prototype.keyUpHandler_ = function(e) {
    if (e.altKey && String.fromCharCode(e.keyCode).toLowerCase() == 'd') {
        if (this.debugModeOn_) {
            goog.style.uninstallStyles(this.debugModeOn_);
            this.debugModeOn_ = null;
        } else {
            this.debugModeOn_ = goog.style.installStyles('.lime-scene div,.lime-scene img,' +
                '.lime-scene canvas{border: 1px solid #c00;}');
        }
        e.stopPropagation();
        e.preventDefault();
    }
}

/**
 * @inheritDoc
 */
lime.Director.prototype.hitTest = function(e) {
    if (e && e.screenPosition)
        e.position = this.screenToLocal(e.screenPosition);
    return true;
};