/**
 * @private
 * Handle batch read / write of DOMs, currently used in SizeMonitor + PaintMonitor
 */
Ext.define('Ext.TaskQueue', {
    requires: 'Ext.AnimationQueue',
 
    singleton: true,
 
    pending: false,
 
    mode: true, // true for 'read', false for 'write'
 
    //<debug>
    protectedReadQueue: [],
    protectedWriteQueue: [],
    //</debug>
 
    readQueue: [],
    writeQueue: [],
    readRequestId: 0,
    writeRequestId: 0,
 
    timer: null,
 
    constructor: function() {
        var me = this;
 
        me.run = me.run.bind(me);
 
        // Some global things like floated wrapper are persistent and will add tasks/
        // add timers all the time, spoiling resource checks in our unit test suite.
        // To work around that we're implementing a parallel queue where only trusted
        // tasks will go, and fly under the radar of resource checker.
        //<debug>
        me.runProtected = Ext.Function.bind(
            me.run, me, [me.protectedReadQueue, me.protectedWriteQueue, 'runProtected']
        );
        me.runProtected.$skipTimerCheck = true;
        //</debug>
 
        // iOS has a nasty bug which causes pending requestAnimationFrame to not release
        // the callback when the WebView is switched back and forth from / to being background 
        // process. We use a watchdog timer to workaround this, and restore the pending state
        // correctly if this happens. This timer has to be set as an interval from the very
        // beginning and we have to keep it running for as long as the app lives, setting it later
        // doesn't seem to work.
        // The watchdog timer must be accessible for environments to cancel.
        if (Ext.os.is.iOS) {
            //<debug>
            me.watch.$skipTimerCheck = true;
            //</debug>
            me.watchdogTimer = Ext.interval(this.watch, 500, this);
        }
    },
 
    requestRead: function(fn, scope, args) {
        var request = {
            id: ++this.readRequestId,
            fn: fn,
            scope: scope,
            args: args
        };
 
        //<debug>
        if (arguments[3] === true) {
            this.protectedReadQueue.push(request);
            this.request(true, 'runProtected');
        }
        else {
        //</debug>
            this.readQueue.push(request);
            this.request(true);
        //<debug>
        }
        //</debug>
 
        return request.id;
    },
 
    cancelRead: function(id) {
        this.cancelRequest(this.readQueue, id, true);
    },
 
    requestWrite: function(fn, scope, args) {
        var me = this,
            request = {
                id: ++me.writeRequestId,
                fn: fn,
                scope: scope,
                args: args
            };
 
        //<debug>
        if (arguments[3] === true) {
            me.protectedWriteQueue.push(request);
            me.request(false, 'runProtected');
        }
        else {
        //</debug>
            me.writeQueue.push(request);
            me.request(false);
        //<debug>
        }
        //</debug>
 
        return request.id;
    },
 
    cancelWrite: function(id) {
        this.cancelRequest(this.writeQueue, id, false);
    },
 
    request: function(mode, method) {
        var me = this;
 
        //<debug>
        // Used below to cancel the correct timer.
        /* eslint-disable-next-line one-var */
        var oldMode = me.mode;
        //</debug>
 
        if (!me.pending) {
            me.pendingTime = Date.now();
            me.pending = true;
            me.mode = mode;
 
            if (mode) {
                me.timer = Ext.defer(me[method] || me.run, 1);
            }
            else {
                me.timer = Ext.raf(me[method] || me.run);
            }
        }
 
        //<debug>
        // Last one should win
        if (me.mode === mode && me.timer) {
            if (oldMode) {
                Ext.undefer(me.timer);
            }
            else {
                Ext.unraf(me.timer);
            }
 
            if (mode) {
                me.timer = Ext.defer(me[method] || me.run, 1);
            }
            else {
                me.timer = Ext.raf(me[method] || me.run);
            }
        }
        //</debug>
    },
 
    cancelRequest: function(queue, id, mode) {
        var i;
 
        for (= 0; i < queue.length; i++) {
            if (queue[i].id === id) {
                queue.splice(i, 1);
 
                break;
            }
        }
 
        if (!queue.length && this.mode === mode && this.timer) {
            Ext.undefer(this.timer);
        }
    },
 
    watch: function() {
        if (this.pending && Date.now() - this.pendingTime >= 500) {
            this.run();
        }
    },
 
    run: function(readQueue, writeQueue, method) {
        var me = this,
            mode = null,
            queue, tasks, task, fn, scope, args, i, len;
 
        readQueue = readQueue || me.readQueue;
        writeQueue = writeQueue || me.writeQueue;
 
        me.pending = false;
 
        me.pending = me.timer = false;
 
        if (me.mode) {
            queue = readQueue;
 
            if (writeQueue.length > 0) {
                mode = false;
            }
        }
        else {
            queue = writeQueue;
 
            if (readQueue.length > 0) {
                mode = true;
            }
        }
 
        tasks = queue.slice();
        queue.length = 0;
 
        for (= 0, len = tasks.length; i < len; i++) {
            task = tasks[i];
 
            fn = task.fn;
            scope = task.scope;
            args = task.args;
 
            if (scope && (scope.destroying || scope.destroyed)) {
                continue;
            }
 
            if (typeof fn === 'string') {
                fn = scope[fn];
            }
 
            if (args) {
                fn.apply(scope, args);
            }
            else {
                fn.call(scope);
            }
        }
 
        tasks.length = 0;
 
        if (mode !== null) {
            me.request(mode, method);
        }
    },
 
    clear: function() {
        var me = this,
            timer = me.timer;
 
        if (timer) {
            if (me.mode) {
                Ext.undefer(timer);
            }
            else {
                Ext.unraf(timer);
            }
        }
 
        me.readQueue.length = me.writeQueue.length = 0;
        me.pending = me.timer = false;
        me.mode = true;
    }
 
    //<debug>
    /* eslint-disable-next-line comma-style */
    , privates: {
        flush: function() {
            var me = this,
                mode = me.mode;
 
            while (me.readQueue.length || me.writeQueue.length) {
                if (mode) {
                    Ext.undefer(me.timer);
                }
                else {
                    Ext.unraf(me.timer);
                }
 
                me.run();
            }
 
            me.mode = true;
        }
    }
    //</debug>
});