// @tag class
/**
 *
 * @since 6.7.0
 * @private
 */
Ext.define('Ext.mixin.Watchable', {
    on: function(name, fn, scope) {
        return this._watchUpdate(false, '_watchAdd', name, fn, scope);
    },
 
    fire: function(event, args) {
        var me = this,
            watching = me.watching,
            watchers = watching && watching[event],
            fn, i, r, scope;
 
        if (watchers) {
            ++watchers.$firing;
 
            for (= 0; i < watchers.length; ++i) {
                scope = watchers[i][0];
                fn = watchers[i][1];
 
                if (fn.charAt) {
                    r = args ? scope[fn].apply(scope, args) : scope[fn]();
                }
                else {
                    r = args ? fn.apply(scope, args) : fn.call(scope);
                }
 
                if (=== false) {
                    return r;
                }
            }
 
            --watchers.$firing;
        }
    },
 
    fireEvent: function() {
        var args = Ext.Array.slice(arguments),
            event = args.shift();
 
        return this.fire(event, args);
    },
 
    un: function(name, fn, scope) {
        return this._watchUpdate(true, '_watchRemove', name, fn, scope);
    },
 
    privates: {
        watching: null,
 
        $watchOptions: {
            destroyable: 1,
            scope: 1
        },
 
        _watchAdd: function(watching, name, fn, scope, destroyable) {
            //<debug>
            if (typeof fn === 'string' && !scope[fn]) {
                Ext.raise('No such method "' + fn + '" on ' + scope.$className);
            }
            //</debug>
 
            // eslint-disable-next-line vars-on-top
            var watchers = watching[name],
                entry = [scope, fn],
                i, ent;
 
            if (!watchers) {
                watching[name] = watchers = [];
                watchers.$firing = 0;
            }
            else {
                // If the scope/fn pair is already registered, don't duplicate it.
                for (= watchers.length; i-- > 0; /* empty */) {
                    ent = watchers[i];
 
                    if (fn === ent[1]) {
                        if (scope ? ent[0] === scope : !ent[0]) {
                            return;
                        }
                    }
                }
 
                if (watchers.$firing) {
                    watching[name] = watchers = watchers.slice();
                    watchers.$firing = 0;
                }
            }
 
            watchers.push(entry);
 
            if (destroyable) {
                entry.push(name);
                destroyable.items.push(entry);
            }
        },
 
        _watchRemove: function(watching, name, fn, scope) {
            var watchers = watching[name],
                i;
 
            if (watchers) {
                if (watchers.$firing) {
                    watching[name] = watchers = watchers.slice();
                    watchers.$firing = 0;
                }
 
                for (= watchers.length; i-- > 0; /* empty */) {
                    if (watchers[i][0] === scope && watchers[i][1] === fn) {
                        watchers.splice(i, 1);
                    }
                }
            }
        },
 
        _watchUpdate: function(remove, process, name, fn, scope) {
            var me = this,
                watch = name,
                watching = me.watching,
                destroyable;
 
            if (!watching) {
                if (remove) {
                    return;
                }
 
                me.watching = watching = {};
            }
 
            if (typeof name === 'string') {
                me[process](watching, name, fn, scope);
            }
            else {
                destroyable = watch.destroyable
                    ? { owner: me, items: [], destroy: me._watcherDestroyer }
                    : null;
 
                scope = watch.scope;
 
                for (name in watch) {
                    if (!me.$watchOptions[name]) {
                        me[process](watching, name, watch[name], scope, destroyable);
                    }
                }
            }
 
            return destroyable;
        },
 
        _watcherDestroyer: function() {
            var me = this.owner,
                watching = me.watching,
                items = this.items,
                entry, i;
 
            for (= 0; i < items.length; ++i) {
                entry = items[i];
 
                me._watchRemove(watching, entry[2], entry[1], entry[0]);
            }
        }
    }
});