/**
 * This class makes buffered methods simple and also handles cleanup on `destroy`.
 *
 *      Ext.define('Foo', {
 *          mixins: [
 *              'Ext.mixin.Bufferable'
 *          ],
 *
 *          bufferableMethods: {
 *              // Provides a "foobar" method that calls "doFoobar" with the
 *              // most recent arguments but delayed by 50ms from the last
 *              // call. Calls to "foobar" made during the 50ms wait restart
 *              // the timer and replace the arguments.
 *
 *              foobar: 50
 *          },
 *
 *          method: function() {
 *              this.foobar(42);  // call doFoobar in 50ms
 *
 *              if (this.isFoobarPending) {
 *                  // test if "foobar" is pending
 *              }
 *
 *              this.flushFoobar();  // actually, call it now
 *
 *              this.cancelFoobar(); // or never mind
 *          },
 *
 *          doFoobar: function() {
 *              // time to do the "foobar" thing
 *          }
 *      });
 *
 * @since 6.5.0
 * @private
 */
Ext.define('Ext.mixin.Bufferable', function(Bufferable) { return { // eslint-disable-line brace-style, max-len
    extend: 'Ext.Mixin',
 
    mixinConfig: {
        id: 'bufferable',
 
        before: {
            destroy: 'cancelAllCalls'
        },
 
        extended: function(baseClass, derivedClass, classBody) {
            var bufferableMethods = classBody.bufferableMethods;
 
            if (bufferableMethods) {
                delete classBody.bufferableMethods;
 
                Bufferable.processClass(derivedClass, bufferableMethods);
            }
        }
    },
 
    afterClassMixedIn: function(targetClass) {
        Bufferable.processClass(targetClass);
    },
 
    privates: {
        /**
         * Cancel all pending `bufferableMethod` calls on this object.
         * @since 6.5.0
         * @private
         */
        cancelAllCalls: function() {
            var bufferables = this.bufferables,
                name;
 
            if (bufferables) {
                for (name in bufferables) {
                    bufferables[name].cancel();
                    delete bufferables[name];
                }
            }
        },
 
        /**
         * Cancel a specific pending `bufferableMethod` call on this object.
         * @param {String} name The name of the buffered method to cancel.
         * @param {Boolean} invoke (private)
         * @return {Boolean} Returns `true` if a cancellation occurred.
         * @since 6.5.0
         * @private
         */
        cancelBufferedCall: function(name, invoke) {
            var bufferables = this.bufferables,
                timer = bufferables && bufferables[name];
 
            if (timer) {
                timer[invoke ? 'invoke' : 'cancel']();
            }
 
            return !!timer;
        },
 
        /**
         * Flushes a specific pending `bufferableMethod` call on this object if one is
         * pending.
         * @param {String} name The name of the buffered method to cancel.
         * @return {Boolean} Returns `true` if a flush occurred.
         * @since 6.5.0
         * @private
         */
        flushBufferedCall: function(name) {
            return this.cancelBufferedCall(name, true);
        },
 
        /**
         * This method initializes an instance when the first bufferable method is called.
         * It merges an instance-level `bufferableMethods` config if present. This allows
         * an instance to change the buffer timeouts, even to 0 to disable buffering.
         *
         *      Ext.create({
         *          ...
         *          bufferableMethods: {
         *              foobar: 0
         *          }
         *      });
         *
         * Note, this method cannot effect unbuffered methods. The `bufferableMethods`
         * config only instruments buffered methods when used on a class declaration.
         *
         * @return {Object} 
         * @since 6.5.0
         * @private
         */
        initBufferables: function() {
            var me = this,
                methods = me.hasOwnProperty('bufferableMethods') && me.bufferableMethods,
                classMethods;
 
            if (methods) {
                Bufferable._canonicalize(methods);
 
                classMethods = me.self.prototype.bufferableMethods;
 
                me.bufferableMethods = Ext.merge(Ext.clone(classMethods), methods);
            }
 
            return (me.bufferables = {});
        },
 
        /**
         * Returns `true` if a specific `bufferableMethod` is pending.
         * @param {String} name The name of the buffered method to cancel.
         * @return {Boolean} 
         * @since 6.5.0
         * @private
         */
        isCallPending: function(name) {
            var bufferables = this.bufferables,
                timer = bufferables && bufferables[name];
 
            return !!timer;
        },
 
        statics: {
            SINGLE: { single: true },
 
            _canonicalize: function(methods) {
                var t, def, s, name;
 
                for (name in methods) {
                    s = Ext.String.capitalize(name);
                    def = methods[name];
                    t = typeof def;
 
                    if (=== 'number' || t === 'string') {
                        // method: 50
                        // method: 'asap'
                        // method: 'idle'
                        // method: 'raf'
                        methods[name] = def = {
                            delay: def
                        };
                    }
 
                    if (typeof(= def.delay) === 'string') {
                        // method: {
                        //     delay: 'asap'
                        // }
                        def[t] = true;
                        delete def.delay;
                    }
 
                    def.capitalized = s;
                    def.name = name;
 
                    if (!def.fn) {
                        def.fn = 'do' + s;
                    }
 
                    if (!def.flag) {
                        def.flag = 'is' + s + 'Pending';
                    }
                }
            },
 
            _canceller: function() {
                var timer = this, // this fn is "cancel()" on timer instances
                    id = timer.id;
 
                if (id) {
                    if (timer.delay) {
                        Ext.undefer(id);
                    }
                    else if (timer.asap) {
                        Ext.unasap(id);
                    }
                    else if (timer.idle) {
                        Ext.un('idle', id, null, Bufferable.SINGLE);
                    }
                    else if (timer.raf) {
                        Ext.unraf(id);
                    }
 
                    timer.id = null;
                }
 
                timer.args = null;
                timer.target[timer.flag] = false;
            },
 
            _invoker: function() {
                var timer = this, // this fn is "invoke()" on timer instances
                    args = timer.args || Ext.emptyArray,
                    target = timer.target;
 
                //<debug>
                ++timer.invokes;
                //</debug>
 
                timer.cancel();
                target[timer.fn].apply(target, args);
            },
 
            delayCall: function(target, def, args) {
                var bufferables = target.bufferables || target.initBufferables(),
                    name = def.name,
                    timer = bufferables[name] || (bufferables[name] = Ext.apply({
                        //<debug>
                        calls: 0,
                        invokes: 0,
                        //</debug>
                        args: null,
                        cancel: Bufferable._canceller,
                        id: null,
                        target: target,
                        invoke: Bufferable._invoker
                    }, def)),
                    delay = def.delay,
                    exec = function() {
                        if (timer.id) {
                            timer.id = null;
                            timer.invoke();
                        }
                    };
 
                if (timer.id) {
                    timer.cancel();
                }
 
                timer.args = args;
                //<debug>
                ++timer.calls;
                //</debug>
 
                target[timer.flag] = true;
 
                if (delay) {
                    timer.id = Ext.defer(exec, delay);
                }
                else if (def.asap) {
                    timer.id = Ext.asap(exec);
                }
                else if (def.idle) {
                    timer.id = exec;
                    Ext.on('idle', exec, null, Bufferable.SINGLE);
                }
                else if (def.raf) {
                    timer.id = Ext.raf(exec);
                }
                else {
                    // allow bufferableMethods: { foo: 0 } to force immediate call
                    timer.invoke();
                }
            },
 
            processClass: function(cls, bufferableMethods) {
                var proto = cls.prototype,
                    inherited = proto.bufferableMethods,
                    def, name;
 
                if (bufferableMethods) { // if (derived class)
                    Bufferable._canonicalize(bufferableMethods);
 
                    if (inherited) {
                        // If we have a derived class, it could be just adjusting the
                        // configuration, not introducing new properties, so clone the
                        // inherited config and merge on the one from the classBody.
                        inherited = Ext.merge(Ext.clone(inherited), bufferableMethods);
                    }
 
                    proto.bufferableMethods = inherited || bufferableMethods;
                }
                else {
                    // else we are being mixed in, so the bufferableMethods on the
                    // prototype almost certainly belong to the immediate user class
                    // that is mixing us in... (leave the config on the prototype)
                    bufferableMethods = inherited;
                    Bufferable._canonicalize(bufferableMethods);
 
                    // prevent shape change
                    proto.bufferables = null;
                }
 
                if (bufferableMethods) {
                    for (name in bufferableMethods) {
                        if (!proto[name]) {
                            def = bufferableMethods[name];
                            Bufferable.processMethod(proto, def, Array.prototype.slice);
                        }
                    }
                }
            },
 
            processMethod: function(proto, def, slice) {
                var name = def.name,
                    cap = def.capitalized;
 
                proto[name] = function() {
                    return Bufferable.delayCall(this, def, slice.call(arguments));
                };
 
                proto['cancel' + cap] = function() {
                    return this.cancelBufferedCall(name);
                };
 
                proto['flush' + cap] = function() {
                    return this.flushBufferedCall(name);
                };
            }
        } // statics
    } // privates
};
});