/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var Class = require('../utils/Class');
var GetValue = require('../utils/object/GetValue');
var NOOP = require('../utils/NOOP');
var RequestAnimationFrame = require('../dom/RequestAnimationFrame');

//  Frame Rate config
//      fps: {
//          min: 10,
//          target: 60,
//          forceSetTimeOut: false,
//          deltaHistory: 10,
//          panicMax: 120
//     }

// http://www.testufo.com/#test=animation-time-graph

/**
 * @callback TimeStepCallback
 *
 * @param {number} time - The current time. Either a High Resolution Timer value if it comes from Request Animation Frame, or Date.now if using SetTimeout.
 * @param {number} average - The Delta Average.
 * @param {number} interpolation - Interpolation - how far between what is expected and where we are?
 */

/**
 * @classdesc
 * [description]
 *
 * @class TimeStep
 * @memberOf Phaser.Boot
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Game} game - A reference to the Phaser.Game instance that owns this Time Step.
 * @param {FPSConfig} config
 */
var TimeStep = new Class({

    initialize:

    function TimeStep (game, config)
    {
        /**
         * A reference to the Phaser.Game instance.
         *
         * @property {Phaser.Game} game
         * @readOnly
         * @since 3.0.0
         */
        this.game = game;

        /**
         * [description]
         *
         * @property {Phaser.DOM.RequestAnimationFrame} raf
         * @readOnly
         * @since 3.0.0
         */
        this.raf = new RequestAnimationFrame();

        /**
         * A flag that is set once the TimeStep has started running and toggled when it stops.
         *
         * @property {boolean} started
         * @readOnly
         * @default false
         * @since 3.0.0
         */
        this.started = false;

        /**
         * A flag that is set once the TimeStep has started running and toggled when it stops.
         * The difference between this value and `started` is that `running` is toggled when
         * the TimeStep is sent to sleep, where-as `started` remains `true`, only changing if
         * the TimeStep is actually stopped, not just paused.
         *
         * @property {boolean} running
         * @readOnly
         * @default false
         * @since 3.0.0
         */
        this.running = false;

        /**
         * The minimum fps rate you want the Time Step to run at.
         *
         * @property {integer} minFps
         * @default 5
         * @since 3.0.0
         */
        this.minFps = GetValue(config, 'min', 5);

        /**
         * The target fps rate for the Time Step to run at.
         *
         * Setting this value will not actually change the speed at which the browser runs, that is beyond
         * the control of Phaser. Instead, it allows you to determine performance issues and if the Time Step
         * is spiraling out of control.
         *
         * @property {integer} targetFps
         * @default 60
         * @since 3.0.0
         */
        this.targetFps = GetValue(config, 'target', 60);

        /**
         * The minFps value in ms.
         * Defaults to 200ms between frames (i.e. super slow!)
         *
         * @property {number} _min
         * @private
         * @since 3.0.0
         */
        this._min = 1000 / this.minFps;

        /**
         * The targetFps value in ms.
         * Defaults to 16.66ms between frames (i.e. normal)
         *
         * @property {number} _target
         * @private
         * @since 3.0.0
         */
        this._target = 1000 / this.targetFps;

        /**
         * An exponential moving average of the frames per second.
         *
         * @property {integer} actualFps
         * @readOnly
         * @default 60
         * @since 3.0.0
         */
        this.actualFps = this.targetFps;

        /**
         * [description]
         *
         * @property {integer} nextFpsUpdate
         * @readOnly
         * @default 0
         * @since 3.0.0
         */
        this.nextFpsUpdate = 0;

        /**
         * The number of frames processed this second.
         *
         * @property {integer} framesThisSecond
         * @readOnly
         * @default 0
         * @since 3.0.0
         */
        this.framesThisSecond = 0;

        /**
         * A callback to be invoked each time the Time Step steps.
         *
         * @property {TimeStepCallback} callback
         * @default NOOP
         * @since 3.0.0
         */
        this.callback = NOOP;

        /**
         * You can force the Time Step to use Set Timeout instead of Request Animation Frame by setting
         * the `forceSetTimeOut` property to `true` in the Game Configuration object. It cannot be changed at run-time.
         *
         * @property {boolean} forceSetTimeOut
         * @readOnly
         * @default false
         * @since 3.0.0
         */
        this.forceSetTimeOut = GetValue(config, 'forceSetTimeOut', false);

        /**
         * [description]
         *
         * @property {integer} time
         * @default 0
         * @since 3.0.0
         */
        this.time = 0;

        /**
         * [description]
         *
         * @property {integer} startTime
         * @default 0
         * @since 3.0.0
         */
        this.startTime = 0;

        /**
         * [description]
         *
         * @property {integer} lastTime
         * @default 0
         * @since 3.0.0
         */
        this.lastTime = 0;

        /**
         * [description]
         *
         * @property {integer} frame
         * @readOnly
         * @default 0
         * @since 3.0.0
         */
        this.frame = 0;

        /**
         * [description]
         *
         * @property {boolean} inFocus
         * @readOnly
         * @default true
         * @since 3.0.0
         */
        this.inFocus = true;

        /**
         * [description]
         *
         * @property {integer} _pauseTime
         * @private
         * @default 0
         * @since 3.0.0
         */
        this._pauseTime = 0;

        /**
         * [description]
         *
         * @property {integer} _coolDown
         * @private
         * @default 0
         * @since 3.0.0
         */
        this._coolDown = 0;

        /**
         * [description]
         *
         * @property {integer} delta
         * @default 0
         * @since 3.0.0
         */
        this.delta = 0;

        /**
         * [description]
         *
         * @property {integer} deltaIndex
         * @default 0
         * @since 3.0.0
         */
        this.deltaIndex = 0;

        /**
         * [description]
         *
         * @property {array} deltaHistory
         * @default 0
         * @since 3.0.0
         */
        this.deltaHistory = [];

        /**
         * [description]
         *
         * @property {integer} deltaSmoothingMax
         * @default 10
         * @since 3.0.0
         */
        this.deltaSmoothingMax = GetValue(config, 'deltaHistory', 10);

        /**
         * [description]
         *
         * @property {integer} panicMax
         * @default 120
         * @since 3.0.0
         */
        this.panicMax = GetValue(config, 'panicMax', 120);

        /**
         * The actual elapsed time in ms between one update and the next.
         * Unlike with `delta` no smoothing, capping, or averaging is applied to this value.
         * So please be careful when using this value in calculations.
         *
         * @property {number} rawDelta
         * @default 0
         * @since 3.0.0
         */
        this.rawDelta = 0;
    },

    /**
     * Called when the DOM window.onBlur event triggers.
     *
     * @method Phaser.Boot.TimeStep#blur
     * @since 3.0.0
     */
    blur: function ()
    {
        this.inFocus = false;
    },

    /**
     * Called when the DOM window.onFocus event triggers.
     *
     * @method Phaser.Boot.TimeStep#focus
     * @since 3.0.0
     */
    focus: function ()
    {
        this.inFocus = true;

        this.resetDelta();
    },

    /**
     * Called when the visibility API says the game is 'hidden' (tab switch out of view, etc)
     *
     * @method Phaser.Boot.TimeStep#pause
     * @since 3.0.0
     */
    pause: function ()
    {
        this._pauseTime = window.performance.now();
    },

    /**
     * Called when the visibility API says the game is 'visible' again (tab switch back into view, etc)
     *
     * @method Phaser.Boot.TimeStep#resume
     * @since 3.0.0
     */
    resume: function ()
    {
        this.resetDelta();

        this.startTime += this.time - this._pauseTime;
    },

    /**
     * [description]
     *
     * @method Phaser.Boot.TimeStep#resetDelta
     * @since 3.0.0
     */
    resetDelta: function ()
    {
        var now = window.performance.now();

        this.time = now;
        this.lastTime = now;
        this.nextFpsUpdate = now + 1000;
        this.framesThisSecond = 0;
        this.frame = 0;

        //  Pre-populate smoothing array

        for (var i = 0; i < this.deltaSmoothingMax; i++)
        {
            this.deltaHistory[i] = Math.min(this._target, this.deltaHistory[i]);
        }

        this.delta = 0;
        this.deltaIndex = 0;

        this._coolDown = this.panicMax;
    },

    /**
     * Starts the Time Step running, if it is not already doing so.
     * Called automatically by the Game Boot process.
     *
     * @method Phaser.Boot.TimeStep#start
     * @since 3.0.0
     *
     * @param {TimeStepCallback} callback - The callback to be invoked each time the Time Step steps.
     */
    start: function (callback)
    {
        if (this.started)
        {
            return this;
        }

        this.started = true;
        this.running = true;

        for (var i = 0; i < this.deltaSmoothingMax; i++)
        {
            this.deltaHistory[i] = this._target;
        }

        this.resetDelta();

        this.startTime = window.performance.now();

        this.callback = callback;

        this.raf.start(this.step.bind(this), this.forceSetTimeOut);
    },

    /**
     * The main step method. This is called each time the browser updates, either by Request Animation Frame,
     * or by Set Timeout. It is responsible for calculating the delta values, frame totals, cool down history and more.
     * You generally should never call this method directly.
     *
     * @method Phaser.Boot.TimeStep#step
     * @since 3.0.0
     *
     * @param {integer} time - The current time. Either a High Resolution Timer value if it comes from Request Animation Frame, or Date.now if using SetTimeout.
     */
    step: function (time)
    {
        this.frame++;

        this.rawDelta = time - this.lastTime;

        var idx = this.deltaIndex;
        var history = this.deltaHistory;
        var max = this.deltaSmoothingMax;

        //  delta time (time is in ms)
        var dt = (time - this.lastTime);

        //  When a browser switches tab, then comes back again, it takes around 10 frames before
        //  the delta time settles down so we employ a 'cooling down' period before we start
        //  trusting the delta values again, to avoid spikes flooding through our delta average

        if (this._coolDown > 0 || !this.inFocus)
        {
            this._coolDown--;

            dt = Math.min(dt, this._target);
        }

        if (dt > this._min)
        {
            //  Probably super bad start time or browser tab context loss,
            //  so use the last 'sane' dt value

            dt = history[idx];

            //  Clamp delta to min (in case history has become corrupted somehow)
            dt = Math.min(dt, this._min);
        }

        //  Smooth out the delta over the previous X frames

        //  add the delta to the smoothing array
        history[idx] = dt;

        //  adjusts the delta history array index based on the smoothing count
        //  this stops the array growing beyond the size of deltaSmoothingMax
        this.deltaIndex++;

        if (this.deltaIndex > max)
        {
            this.deltaIndex = 0;
        }

        //  Delta Average
        var avg = 0;

        //  Loop the history array, adding the delta values together

        for (var i = 0; i < max; i++)
        {
            avg += history[i];
        }

        //  Then divide by the array length to get the average delta
        avg /= max;

        //  Set as the world delta value
        this.delta = avg;

        //  Real-world timer advance
        this.time += this.rawDelta;

        // Update the estimate of the frame rate, `fps`. Every second, the number
        // of frames that occurred in that second are included in an exponential
        // moving average of all frames per second, with an alpha of 0.25. This
        // means that more recent seconds affect the estimated frame rate more than
        // older seconds.
        //
        // When a browser window is NOT minimized, but is covered up (i.e. you're using
        // another app which has spawned a window over the top of the browser), then it
        // will start to throttle the raf callback time. It waits for a while, and then
        // starts to drop the frame rate at 1 frame per second until it's down to just over 1fps.
        // So if the game was running at 60fps, and the player opens a new window, then
        // after 60 seconds (+ the 'buffer time') it'll be down to 1fps, so rafin'g at 1Hz.
        //
        // When they make the game visible again, the frame rate is increased at a rate of
        // approx. 8fps, back up to 60fps (or the max it can obtain)
        //
        // There is no easy way to determine if this drop in frame rate is because the
        // browser is throttling raf, or because the game is struggling with performance
        // because you're asking it to do too much on the device.

        if (time > this.nextFpsUpdate)
        {
            //  Compute the new exponential moving average with an alpha of 0.25.
            this.actualFps = 0.25 * this.framesThisSecond + 0.75 * this.actualFps;
            this.nextFpsUpdate = time + 1000;
            this.framesThisSecond = 0;
        }

        this.framesThisSecond++;

        //  Interpolation - how far between what is expected and where we are?
        var interpolation = avg / this._target;

        this.callback(time, avg, interpolation);

        //  Shift time value over
        this.lastTime = time;
    },

    /**
     * Manually calls TimeStep.step, passing in the performance.now value to it.
     *
     * @method Phaser.Boot.TimeStep#tick
     * @since 3.0.0
     */
    tick: function ()
    {
        this.step(window.performance.now());
    },

    /**
     * Sends the TimeStep to sleep, stopping Request Animation Frame (or SetTimeout) and toggling the `running` flag to false.
     *
     * @method Phaser.Boot.TimeStep#sleep
     * @since 3.0.0
     */
    sleep: function ()
    {
        if (this.running)
        {
            this.raf.stop();

            this.running = false;
        }
    },

    /**
     * Wakes-up the TimeStep, restarting Request Animation Frame (or SetTimeout) and toggling the `running` flag to true.
     * The `seamless` argument controls if the wake-up should adjust the start time or not.
     *
     * @method Phaser.Boot.TimeStep#wake
     * @since 3.0.0
     *
     * @param {boolean} [seamless=false] - Adjust the startTime based on the lastTime values.
     */
    wake: function (seamless)
    {
        if (this.running)
        {
            this.sleep();
        }
        else if (seamless)
        {
            this.startTime += -this.lastTime + (this.lastTime = window.performance.now());
        }

        this.raf.start(this.step.bind(this), this.useRAF);

        this.running = true;

        this.step(window.performance.now());
    },

    /**
     * Stops the TimeStep running.
     *
     * @method Phaser.Boot.TimeStep#stop
     * @since 3.0.0
     *
     * @return {Phaser.Boot.TimeStep} The TimeStep object.
     */
    stop: function ()
    {
        this.running = false;
        this.started = false;

        this.raf.stop();

        return this;
    },

    /**
     * Destroys the TimeStep. This will stop Request Animation Frame, stop the step, clear the callbacks and null
     * any objects.
     *
     * @method Phaser.Boot.TimeStep#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.stop();

        this.callback = NOOP;

        this.raf = null;
        this.game = null;
    }

});

module.exports = TimeStep;
