/****************************************************************************
 Copyright (c) 2013-2016 Chukong Technologies Inc.
 Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.

 https://www.cocos.com/

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated engine source code (the "Software"), a limited,
  worldwide, royalty-free, non-assignable, revocable and non-exclusive license
 to use Cocos Creator solely to develop games on your target platforms. You shall
  not use Cocos Creator software for developing other software or tools that's
  used for developing games. You are not granted to publish, distribute,
  sublicense, and/or sell copies of Cocos Creator.

 The software or tools in this License Agreement are licensed, not sold.
 Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ****************************************************************************/

require('./platform/CCClass');
var Flags = require('./platform/CCObject').Flags;
var jsArray = require('./platform/js').array;

var IsStartCalled = Flags.IsStartCalled;
var IsOnEnableCalled = Flags.IsOnEnableCalled;
var IsEditorOnEnableCalled = Flags.IsEditorOnEnableCalled;

var callerFunctor = CC_EDITOR && require('./utils/misc').tryCatchFunctor_EDITOR;
var callOnEnableInTryCatch = CC_EDITOR && callerFunctor('onEnable');
var callOnDisableInTryCatch = CC_EDITOR && callerFunctor('onDisable');

function sortedIndex (array, comp) {
    var order = comp.constructor._executionOrder;
    var id = comp._id;
    for (var l = 0, h = array.length - 1, m = h >>> 1;
         l <= h;
         m = (l + h) >>> 1
    ) {
        var test = array[m];
        var testOrder = test.constructor._executionOrder;
        if (testOrder > order) {
            h = m - 1;
        }
        else if (testOrder < order) {
            l = m + 1;
        }
        else {
            var testId = test._id;
            if (testId > id) {
                h = m - 1;
            }
            else if (testId < id) {
                l = m + 1;
            }
            else {
                return m;
            }
        }
    }
    return ~l;
}

// remove disabled and not invoked component from array
function stableRemoveInactive (iterator, flagToClear) {
    var array = iterator.array;
    var next = iterator.i + 1;
    while (next < array.length) {
        var comp = array[next];
        if (comp._enabled && comp.node && comp.node._activeInHierarchy) {
            ++next;
        }
        else {
            iterator.removeAt(next);
            if (flagToClear) {
                comp._objFlags &= ~flagToClear;
            }
        }
    }
}

// This class contains some queues used to invoke life-cycle methods by script execution order
var LifeCycleInvoker = cc.Class({
    __ctor__ (invokeFunc) {
        var Iterator = jsArray.MutableForwardIterator;
        // components which priority === 0 (default)
        this._zero = new Iterator([]);
        // components which priority < 0
        this._neg = new Iterator([]);
        // components which priority > 0
        this._pos = new Iterator([]);

        if (CC_TEST) {
            cc.assert(typeof invokeFunc === 'function', 'invokeFunc must be type function');
        }
        this._invoke = invokeFunc;
    },
    statics: {
        stableRemoveInactive
    },
    add: null,
    remove: null,
    invoke: null,
});

function compareOrder (a, b) {
    return a.constructor._executionOrder - b.constructor._executionOrder;
}

// for onLoad: sort once all components registered, invoke once
var OneOffInvoker = cc.Class({
    extends: LifeCycleInvoker,
    add (comp) {
        var order = comp.constructor._executionOrder;
        (order === 0 ? this._zero : (order < 0 ? this._neg : this._pos)).array.push(comp);
    },
    remove (comp) {
        var order = comp.constructor._executionOrder;
        (order === 0 ? this._zero : (order < 0 ? this._neg : this._pos)).fastRemove(comp);
    },
    cancelInactive (flagToClear) {
        stableRemoveInactive(this._zero, flagToClear);
        stableRemoveInactive(this._neg, flagToClear);
        stableRemoveInactive(this._pos, flagToClear);
    },
    invoke () {
        var compsNeg = this._neg;
        if (compsNeg.array.length > 0) {
            compsNeg.array.sort(compareOrder);
            this._invoke(compsNeg);
            compsNeg.array.length = 0;
        }

        this._invoke(this._zero);
        this._zero.array.length = 0;

        var compsPos = this._pos;
        if (compsPos.array.length > 0) {
            compsPos.array.sort(compareOrder);
            this._invoke(compsPos);
            compsPos.array.length = 0;
        }
    },
});

// for update: sort every time new component registered, invoke many times
var ReusableInvoker = cc.Class({
    extends: LifeCycleInvoker,
    add (comp) {
        var order = comp.constructor._executionOrder;
        if (order === 0) {
            this._zero.array.push(comp);
        }
        else {
            var array = order < 0 ? this._neg.array : this._pos.array;
            var i = sortedIndex(array, comp);
            if (i < 0) {
                array.splice(~i, 0, comp);
            }
            else if (CC_DEV) {
                cc.error('component already added');
            }
        }
    },
    remove (comp) {
        var order = comp.constructor._executionOrder;
        if (order === 0) {
            this._zero.fastRemove(comp);
        }
        else {
            var iterator = order < 0 ? this._neg : this._pos;
            var i = sortedIndex(iterator.array, comp);
            if (i >= 0) {
                iterator.removeAt(i);
            }
        }
    },
    invoke (dt) {
        if (this._neg.array.length > 0) {
            this._invoke(this._neg, dt);
        }

        this._invoke(this._zero, dt);

        if (this._pos.array.length > 0) {
            this._invoke(this._pos, dt);
        }
    },
});

function enableInEditor (comp) {
    if (!(comp._objFlags & IsEditorOnEnableCalled)) {
        cc.engine.emit('component-enabled', comp.uuid);
        comp._objFlags |= IsEditorOnEnableCalled;
    }
}

// return function to simply call each component with try catch protection
function createInvokeImpl (indiePath, useDt, ensureFlag, fastPath) {
    if (CC_SUPPORT_JIT) {
        // function (it) {
        //     var a = it.array;
        //     for (it.i = 0; it.i < a.length; ++it.i) {
        //         var c = a[it.i];
        //         // ...
        //     }
        // }
        let body = 'var a=it.array;' +
                   'for(it.i=0;it.i<a.length;++it.i){' +
                   'var c=a[it.i];' +
                   indiePath +
                   '}';
        fastPath = useDt ? Function('it', 'dt', body) : Function('it', body);
        indiePath = Function('c', 'dt', indiePath);
    }
    return function (iterator, dt) {
        try {
            fastPath(iterator, dt);
        }
        catch (e) {
            // slow path
            cc._throw(e);
            var array = iterator.array;
            if (ensureFlag) {
                array[iterator.i]._objFlags |= ensureFlag;
            }
            ++iterator.i;   // invoke next callback
            for (; iterator.i < array.length; ++iterator.i) {
                try {
                    indiePath(array[iterator.i], dt);
                }
                catch (e) {
                    cc._throw(e);
                    if (ensureFlag) {
                        array[iterator.i]._objFlags |= ensureFlag;
                    }
                }
            }
        }
    };
}

var invokeStart = CC_SUPPORT_JIT ?
    createInvokeImpl('c.start();c._objFlags|=' + IsStartCalled, false, IsStartCalled) :
    createInvokeImpl(function (c) {
            c.start();
            c._objFlags |= IsStartCalled;
        },
        false,
        IsStartCalled,
        function (iterator) {
            var array = iterator.array;
            for (iterator.i = 0; iterator.i < array.length; ++iterator.i) {
                let comp = array[iterator.i];
                comp.start();
                comp._objFlags |= IsStartCalled;
            }
        }
    );
var invokeUpdate = CC_SUPPORT_JIT ?
    createInvokeImpl('c.update(dt)', true) :
    createInvokeImpl(function (c, dt) {
            c.update(dt);
        },
        true,
        undefined,
        function (iterator, dt) {
            var array = iterator.array;
            for (iterator.i = 0; iterator.i < array.length; ++iterator.i) {
                array[iterator.i].update(dt);
            }
        }
    );
var invokeLateUpdate = CC_SUPPORT_JIT ?
    createInvokeImpl('c.lateUpdate(dt)', true) :
    createInvokeImpl(function (c, dt) {
            c.lateUpdate(dt);
        },
        true,
        undefined,
        function (iterator, dt) {
            var array = iterator.array;
            for (iterator.i = 0; iterator.i < array.length; ++iterator.i) {
                array[iterator.i].lateUpdate(dt);
            }
        }
    );
/**
 * The Manager for Component's life-cycle methods.
 */
function ctor () {
    // invokers
    this.startInvoker = new OneOffInvoker(invokeStart);
    this.updateInvoker = new ReusableInvoker(invokeUpdate);
    this.lateUpdateInvoker = new ReusableInvoker(invokeLateUpdate);

    // components deferred to next frame
    this._deferredComps = [];

    // during a loop
    this._updating = false;
}
var ComponentScheduler = cc.Class({
    ctor: ctor,
    unscheduleAll: ctor,

    statics: {
        LifeCycleInvoker,
        OneOffInvoker,
        createInvokeImpl,
        invokeOnEnable: CC_EDITOR ? function (iterator) {
            var compScheduler = cc.director._compScheduler;
            var array = iterator.array;
            for (iterator.i = 0; iterator.i < array.length; ++iterator.i) {
                let comp = array[iterator.i];
                if (comp._enabled) {
                    callOnEnableInTryCatch(comp);
                    var deactivatedDuringOnEnable = !comp.node._activeInHierarchy;
                    if (!deactivatedDuringOnEnable) {
                        compScheduler._onEnabled(comp);
                    }
                }
            }
        } : function (iterator) {
            var compScheduler = cc.director._compScheduler;
            var array = iterator.array;
            for (iterator.i = 0; iterator.i < array.length; ++iterator.i) {
                let comp = array[iterator.i];
                if (comp._enabled) {
                    comp.onEnable();
                    var deactivatedDuringOnEnable = !comp.node._activeInHierarchy;
                    if (!deactivatedDuringOnEnable) {
                        compScheduler._onEnabled(comp);
                    }
                }
            }
        }
    },

    _onEnabled (comp) {
        cc.director.getScheduler().resumeTarget(comp);
        comp._objFlags |= IsOnEnableCalled;

        // schedule
        if (this._updating) {
            this._deferredComps.push(comp);
        }
        else {
            this._scheduleImmediate(comp);
        }
    },

    _onDisabled (comp) {
        cc.director.getScheduler().pauseTarget(comp);
        comp._objFlags &= ~IsOnEnableCalled;

        // cancel schedule task
        var index = this._deferredComps.indexOf(comp);
        if (index >= 0) {
            jsArray.fastRemoveAt(this._deferredComps, index);
            return;
        }

        // unschedule
        if (comp.start && !(comp._objFlags & IsStartCalled)) {
            this.startInvoker.remove(comp);
        }
        if (comp.update) {
            this.updateInvoker.remove(comp);
        }
        if (comp.lateUpdate) {
            this.lateUpdateInvoker.remove(comp);
        }
    },

    enableComp: CC_EDITOR ? function (comp, invoker) {
        if (cc.engine.isPlaying || comp.constructor._executeInEditMode) {
            if (!(comp._objFlags & IsOnEnableCalled)) {
                if (comp.onEnable) {
                    if (invoker) {
                        invoker.add(comp);
                        enableInEditor(comp);
                        return;
                    }
                    else {
                        callOnEnableInTryCatch(comp);

                        var deactivatedDuringOnEnable = !comp.node._activeInHierarchy;
                        if (deactivatedDuringOnEnable) {
                            return;
                        }
                    }
                }
                this._onEnabled(comp);
            }
        }
        enableInEditor(comp);
    } : function (comp, invoker) {
        if (!(comp._objFlags & IsOnEnableCalled)) {
            if (comp.onEnable) {
                if (invoker) {
                    invoker.add(comp);
                    return;
                }
                else {
                    comp.onEnable();

                    var deactivatedDuringOnEnable = !comp.node._activeInHierarchy;
                    if (deactivatedDuringOnEnable) {
                        return;
                    }
                }
            }
            this._onEnabled(comp);
        }
    },

    disableComp: CC_EDITOR ? function (comp) {
        if (cc.engine.isPlaying || comp.constructor._executeInEditMode) {
            if (comp._objFlags & IsOnEnableCalled) {
                if (comp.onDisable) {
                    callOnDisableInTryCatch(comp);
                }
                this._onDisabled(comp);
            }
        }
        if (comp._objFlags & IsEditorOnEnableCalled) {
            cc.engine.emit('component-disabled', comp.uuid);
            comp._objFlags &= ~IsEditorOnEnableCalled;
        }
    } : function (comp) {
        if (comp._objFlags & IsOnEnableCalled) {
            if (comp.onDisable) {
                comp.onDisable();
            }
            this._onDisabled(comp);
        }
    },

    _scheduleImmediate (comp) {
        if (typeof comp.start === 'function' && !(comp._objFlags & IsStartCalled)) {
            this.startInvoker.add(comp);
        }
        if (typeof comp.update === 'function') {
            this.updateInvoker.add(comp);
        }
        if (typeof comp.lateUpdate === 'function') {
            this.lateUpdateInvoker.add(comp);
        }
    },

    _deferredSchedule () {
        var comps = this._deferredComps;
        for (var i = 0, len = comps.length; i < len; i++) {
            this._scheduleImmediate(comps[i]);
        }
        comps.length = 0;
    },

    // Call new registered start schedule immediately since last time start phase calling in this frame
    // See cocos-creator/2d-tasks/issues/256
    _startForNewComps () {
        if (this._deferredComps.length > 0) {
            this._deferredSchedule();
            this.startInvoker.invoke();
        }
    },

    startPhase () {
        // Start of this frame
        this._updating = true;

        // call start
        this.startInvoker.invoke();

        // Start components of new activated nodes during start
        this._startForNewComps();

        // if (CC_PREVIEW) {
        //     try {
        //         this.startInvoker.invoke();
        //     }
        //     catch (e) {
        //         // prevent start from getting into infinite loop
        //         this.startInvoker._neg.array.length = 0;
        //         this.startInvoker._zero.array.length = 0;
        //         this.startInvoker._pos.array.length = 0;
        //         throw e;
        //     }
        // }
        // else {
        //     this.startInvoker.invoke();
        // }
    },

    updatePhase (dt) {
        this.updateInvoker.invoke(dt);
    },

    lateUpdatePhase (dt) {
        this.lateUpdateInvoker.invoke(dt);

        // End of this frame
        this._updating = false;

        // Start components of new activated nodes during update and lateUpdate
        // The start callback will be invoked immediately,
        // update and lateUpdate callback will be running in the next frame
        this._startForNewComps();
    },
});

module.exports = ComponentScheduler;