/**
 * This class is a base class for mixins. These are classes that extend this class and are
 * designed to be used as a `mixin` by user code.
 *
 * It provides mixins with the ability to "hook" class methods of the classes in to which
 * they are mixed. For example, consider the `destroy` method pattern. If a mixin class
 * had cleanup requirements, it would need to be called as part of `destroy`.
 * 
 * Starting with a basic class we might have:
 * 
 *      Ext.define('Foo.bar.Base', {
 *          destroy: function() {
 *              console.log('B');
 *              // cleanup
 *          }
 *      });
 *
 * A derived class would look like this:
 *
 *      Ext.define('Foo.bar.Derived', {
 *          extend: 'Foo.bar.Base',
 *
 *          destroy: function() {
 *              console.log('D');
 *              // more cleanup
 *
 *              this.callParent(); // let Foo.bar.Base cleanup as well
 *          }
 *      });
 *
 * To see how using this class help, start with a "normal" mixin class that also needs to
 * cleanup its resources. These mixins must be called explicitly by the classes that use
 * them. For example:
 * 
 *      Ext.define('Foo.bar.Util', {
 *          destroy: function() {
 *              console.log('U');
 *          }
 *      });
 * 
 *      Ext.define('Foo.bar.Derived', {
 *          extend: 'Foo.bar.Base',
 *
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              console.log('D');
 *              // more cleanup
 *
 *              this.mixins.util.destroy.call(this);
 *
 *              this.callParent(); // let Foo.bar.Base cleanup as well
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs D then U then B
 *
 * This class is designed to solve the above in simpler and more reliable way.
 *
 * ## mixinConfig
 * 
 * Using `mixinConfig` the mixin class can provide "before" or "after" hooks that do not
 * involve the derived class implementation. This also means the derived class cannot
 * adjust parameters to the hook methods.
 * 
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              after: {
 *                  destroy: 'destroyUtil'
 *              }
 *          },
 *          
 *          destroyUtil: function() {
 *              console.log('U');
 *          }
 *      });
 * 
 *      Ext.define('Foo.bar.Class', {
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              console.log('D');
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs D then U
 * 
 *  If the destruction should occur in the other order, you can use `before`:
 * 
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              before: {
 *                  destroy: 'destroyUtil'
 *              }
 *          },
 *          
 *          destroyUtil: function() {
 *              console.log('U');
 *          }
 *      });
 * 
 *      Ext.define('Foo.bar.Class', {
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              console.log('D');
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs U then D
 *
 * ### Configs
 *
 * Normally when a mixin defines `config` properties and the class into which the mixin is
 * initially mixed needs to specify values for those configs, the class processor does not
 * yet recognize these config and instead retains the properties on te target class
 * prototype.
 *
 * Changing the above behavior would potentially break application code, so this class
 * provides a way to remedy this:
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              configs: true
 *          },
 *
 *          config: {
 *              foo: null
 *          }
 *      });
 *
 * Now the direct user class can correctly specify `foo` config properties:
 *
 *      Ext.define('Some.other.Class', {
 *          mixins: [
 *              'Foo.bar.Util'
 *          ],
 *
 *          foo: 'bar'  // recognized as the foo config form Foo.bar.Util
 *      });
 *
 * ### Chaining
 *
 * One way for a mixin to provide methods that act more like normal inherited methods is
 * to use an `on` declaration. These methods will be injected into the `callParent` chain
 * between the derived and superclass. For example:
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              on: {
 *                  destroy: function() {
 *                      console.log('M');
 *                  }
 *              }
 *          }
 *      });
 *
 *      Ext.define('Foo.bar.Base', {
 *          destroy: function() {
 *              console.log('B');
 *          }
 *      });
 *
 *      Ext.define('Foo.bar.Derived', {
 *          extend: 'Foo.bar.Base',
 *
 *          mixins: {
 *              util: 'Foo.bar.Util'
 *          },
 *
 *          destroy: function() {
 *              this.callParent();
 *              console.log('D');
 *          }
 *      });
 *
 *      var obj = new Foo.bar.Derived();
 *
 *      obj.destroy();
 *      // logs M then B then D
 *
 * As with `before` and `after`, the value of `on` can be a method name.
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              on: {
 *                  destroy: 'onDestroy'
 *              }
 *          }
 *
 *          onDestroy: function() {
 *              console.log('M');
 *          }
 *      });
 *
 * Because this technique leverages `callParent`, the derived class controls the time and
 * parameters for the call to all of its bases (be they `extend` or `mixin` flavor).
 *
 * ### Derivations
 *
 * Some mixins need to process class extensions of their target class. To do this you can
 * define an `extended` method like so:
 *
 *      Ext.define('Foo.bar.Util', {
 *          extend: 'Ext.Mixin',
 *
 *          mixinConfig: {
 *              extended: function(baseClass, derivedClass, classBody) {
 *                  // This function is called whenever a new "derivedClass" is created
 *                  // that extends a "baseClass" in to which this mixin was mixed.
 *              }
 *          }
 *      });
 *
 * @protected
 */
Ext.define('Ext.Mixin', function(Mixin) { return { // eslint-disable-line brace-style
    statics: {
        addHook: function(hookFn, targetClass, methodName, mixinClassPrototype) {
            var isFunc = Ext.isFunction(hookFn),
                hook = function() {
                    var a = arguments,
                        fn = isFunc ? hookFn : mixinClassPrototype[hookFn],
                        result = this.callParent(a);
                    
                    fn.apply(this, a);
                    
                    return result;
                },
                existingFn = targetClass.hasOwnProperty(methodName) &&
                             targetClass[methodName];
 
            if (isFunc) {
                hookFn.$previous = Ext.emptyFn; // no callParent for these guys
            }
 
            hook.$name = methodName;
            hook.$owner = targetClass.self;
 
            if (existingFn) {
                hook.$previous = existingFn.$previous;
                existingFn.$previous = hook;
            }
            else {
                targetClass[methodName] = hook;
            }
        }
    },
 
    onClassExtended: function(cls, data) {
        var mixinConfig = data.mixinConfig,
            hooks = data.xhooks,
            superclass = cls.superclass,
            onClassMixedIn = data.onClassMixedIn,
            afterClassMixedIn = data.afterClassMixedIn,
            afters, befores, configs, extended, mixed, parentMixinConfig;
 
        if (hooks) {
            // Legacy way
            delete data.xhooks;
            (mixinConfig || (data.mixinConfig = mixinConfig = {})).on = hooks;
        }
 
        if (mixinConfig) {
            parentMixinConfig = superclass.mixinConfig;
 
            if (parentMixinConfig) {
                data.mixinConfig = mixinConfig = Ext.merge({}, parentMixinConfig, mixinConfig);
            }
 
            data.mixinId = mixinConfig.id;
 
            //<debug>
            if (mixinConfig.beforeHooks) {
                Ext.raise('Use of "beforeHooks" is deprecated - use "before" instead');
            }
            
            if (mixinConfig.hooks) {
                Ext.raise('Use of "hooks" is deprecated - use "after" instead');
            }
            
            if (mixinConfig.afterHooks) {
                Ext.raise('Use of "afterHooks" is deprecated - use "after" instead');
            }
            //</debug>
 
            afters = mixinConfig.after;
            befores = mixinConfig.before;
            configs = mixinConfig.configs;
            extended = mixinConfig.extended;
            hooks = mixinConfig.on;
            mixed = mixinConfig.mixed;
        }
 
        if (afters || befores || hooks || extended) {
            // Note: tests are with Ext.Class
            data.onClassMixedIn = function(targetClass) {
                var mixin = this.prototype,
                    targetProto = targetClass.prototype,
                    key;
 
                if (befores) {
                    Ext.Object.each(befores, function(key, value) {
                        targetClass.addMember(key, function() {
                            if (mixin[value].apply(this, arguments) !== false) {
                                return this.callParent(arguments);
                            }
                        });
                    });
                }
 
                if (afters) {
                    Ext.Object.each(afters, function(key, value) {
                        targetClass.addMember(key, function() {
                            var ret = this.callParent(arguments);
                            
                            mixin[value].apply(this, arguments);
                            
                            return ret;
                        });
                    });
                }
 
                if (hooks) {
                    for (key in hooks) {
                        Mixin.addHook(hooks[key], targetProto, key, mixin);
                    }
                }
 
                if (extended) {
                    targetClass.onExtended(function() {
                        var args = Ext.Array.slice(arguments, 0);
                        
                        args.unshift(targetClass);
                        
                        return extended.apply(this, args);
                    }, this);
                }
 
                if (onClassMixedIn) {
                    onClassMixedIn.apply(this, arguments);
                }
            };
        }
 
        if (configs || mixed) {
            data.afterClassMixedIn = function(targetClass) {
                if (configs) {
                    // eslint-disable-next-line vars-on-top
                    var proto = targetClass.prototype,
                        hoistable = this.$config.configs,
                        cfg, name, hoist;
 
                    for (name in proto) {
                        cfg = hoistable[name];
 
                        if (cfg && cfg.isConfig && proto.hasOwnProperty(name)) {
                            (hoist || (hoist = {}))[name] = proto[name];
                            delete proto[name];
                        }
                    }
 
                    if (hoist) {
                        targetClass.$config.add(hoist);
                    }
                }
 
                if (afterClassMixedIn) {
                    afterClassMixedIn.apply(this, arguments);
                }
 
                if (mixed) {
                    mixed.apply(this, arguments);
                }
            };
        }
    }
};}); // eslint-disable-line block-spacing, brace-style