/**
 * This mixin allows a class to easily forward (or proxy) configs to other objects. Once
 * mixed in, the using class (and its derived classes) can add a `proxyConfig` object
 * property to their class body that specifies the accessor and configs to manage.
 *
 * For example:
 *
 *      Ext.define('ParentThing', {
 *          mixins: [
 *              'Ext.mixin.ConfigProxy'
 *          ],
 *
 *          config: {
 *              childThing: {
 *                  xtype: 'panel'
 *              }
 *          },
 *
 *          proxyConfig: {
 *              // The keys of this object are themselves configs. Their getters
 *              // are used to identify the target to which the listed configs are
 *              // proxied.
 *
 *              childThing: [
 *                  // This list of config names will be proxied to the object
 *                  // returned by the getter (getChildThing in this case). In
 *                  // addition, each of these will be defined as configs on this
 *                  // class but with a special getter and setter.
 *                  //
 *                  // These configs cannot be previously defined nor can their
 *                  // be getters or setters already present.
 *
 *                  'title'
 *              ]
 *          }
 *      });
 *
 * If the getter for a proxy target returns `null`, the setter for the proxied config
 * will simply discard the value. It is expected that the target will generally always
 * exist.
 *
 * To proxy methods, the array of config names is replaced by an object:
 *
 *      Ext.define('ParentThing', {
 *          mixins: [
 *              'Ext.mixin.ConfigProxy'
 *          ],
 *
 *          config: {
 *              childThing: {
 *                  xtype: 'panel'
 *              }
 *          },
 *
 *          proxyConfig: {
 *              // The keys of this object are themselves configs. Their getters
 *              // are used to identify the target to which the listed configs are
 *              // proxied.
 *
 *              childThing: {
 *                  configs: [
 *                      // same as when "childThing" was just this array...
 *                  ],
 *
 *                  methods: [
 *                      // A list of methods to proxy to the childThing.
 *                      'doStuff'
 *                  ]
 *              ]
 *          }
 *      });
 *
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.mixin.ConfigProxy', function(ConfigProxy) { return { // eslint-disable-line brace-style, max-len
    extend: 'Ext.Mixin',
 
    mixinConfig: {
        id: 'configproxy',
 
        extended: function(baseClass, derivedClass, classBody) {
            var proxyConfig = classBody.proxyConfig;
 
            derivedClass.$configProxies = Ext.apply(
                {}, derivedClass.superclass.self.$configProxies
            );
 
            if (proxyConfig) {
                delete classBody.proxyConfig;
                ConfigProxy.processClass(derivedClass, proxyConfig);
            }
        }
    },
 
    onClassMixedIn: function(targetClass) {
        var prototype = targetClass.prototype,
            proxyConfig = prototype.proxyConfig,
            initConfig = prototype.initConfig;
 
        prototype.$proxiedConfigs = null;  // constant shape
        targetClass.$configProxies = {
            // contents are basically the same as the proxyConfig object.
        };
 
        prototype.initConfig = function(config) {
            initConfig.apply(this, arguments);
 
            // ensure future setter calls will pass through to the target:
            this.$proxiedConfigs = null;
 
            return this;
        };
 
        if (proxyConfig) {
            delete prototype.proxyConfig;
            ConfigProxy.processClass(targetClass, proxyConfig);
        }
    },
 
    /**
     * This method returns an object of all proxied config values for a given target. This
     * is only useful during the class initialization phase to avoid passing in "wrong"
     * initial config values for a child object and then proxying down all the configs
     * from the parent.
     *
     * This method is not typically called directly but rather `mergeProxiedConfigs` is
     * more likely.
     * @param {String} name The proxy target config name (in the class example, this would
     * be "childThing").
     * @return {Object} 
     * @private
     * @since 6.5.0
     */
    getProxiedConfigs: function(name) {
        var me = this,
            configs = me.config,  // the merged config set
            configProxies = me.self.$configProxies[name],
            i = configProxies && configProxies.length,
            cfg, proxiedConfigs, ret, s, v;
 
        if (&& me.isConfiguring) {
            // Lazily create the $proxiedConfigs map to track the config properties
            // we are "stealing" away.
            proxiedConfigs = me.$proxiedConfigs || (me.$proxiedConfigs = {});
 
            while (i-- > 0) {
                cfg = configProxies[i];
                proxiedConfigs[= cfg.name] = cfg;
 
                if ((= configs[s]) !== undefined) {
                    (ret || (ret = {}))[s] = v;
                }
            }
        }
 
        return ret;
    },
 
    /**
     * This method accepts the normal config object (`itemConfig`) for the child object
     * (`name`) and merges any proxied configs into a new config object. This is useful
     * during the class initialization phase to avoid passing in "wrong" initial config
     * values for a child object and then proxying down the rest of the configs.
     *
     * This method is typically called during an "applier" method for a proxy target. If
     * called at any other time this method simply returns the given `itemConfig`. This
     * makes it safe to code such appliers as follows:
     *
     *      applyChildThing: function(config) {
     *          config = this.mergeProxiedConfigs('childThing', config);
     *
     *          return new ChildThing(config);
     *      }
     *
     * @param {String} name The proxy target config name (in the class example, this would
     * be "childThing").
     * @param {Mixed} itemConfig The default configuration for the child item.
     * @param {Boolean} [alwaysClone] Pass `true` to ensure a new object is returned.
     * @return {Object} 
     * @private
     * @since 6.5.0
     */
    mergeProxiedConfigs: function(name, itemConfig, alwaysClone) {
        var me = this,
            ret = itemConfig,
            proxied = me.getProxiedConfigs(name),
            configurator;
 
        if (proxied) {
            if (!itemConfig) {
                ret = proxied;
            }
            else if (itemConfig.constructor === Object) {
                configurator = me.self.getConfigurator();
 
                // First clone() so don't mutate the config:
                ret = configurator.merge(me, Ext.clone(itemConfig), proxied);
            }
        }
 
        if (alwaysClone && ret === itemConfig) {
            ret = Ext.clone(ret);
        }
 
        return ret;
    },
 
    statics: {
        processClass: function(targetClass, proxyConfig) {
            var ExtConfig = Ext.Config,
                targetProto = targetClass.prototype,
                add = {},
                proxies = targetClass.$configProxies,
                cfg, configs, itemGetter, i, item, methods, n, name, proxiedConfigs, s;
 
            for (item in proxyConfig) {
                itemGetter = ExtConfig.get(item).names.get;
                configs = proxyConfig[item];
 
                if (Ext.isArray(configs)) {
                    methods = null;
                }
                else {
                    methods = configs.methods;
                    configs = configs.configs;
                }
 
                if (!(proxiedConfigs = proxies[item])) {
                    proxies[item] = proxiedConfigs = [];
                }
                else {
                    // this array comes from the superclass so slice it for this class:
                    proxies[item] = proxiedConfigs = proxiedConfigs.slice();
                }
 
                for (= 0, n = methods && methods.length; i < n; ++i) {
                    if (!targetProto[name = methods[i]]) {
                        targetProto[name] = ConfigProxy.wrapFn(itemGetter, name);
                    }
                    //<debug>
                    else {
                        Ext.raise('Cannot proxy method "' + name + '"');
                    }
                    //</debug>
                }
 
                for (= 0, n = configs && configs.length; i < n; ++i) {
                    cfg = ExtConfig.get(= configs[i]);
 
                    //<debug>
                    if (in add) {
                        Ext.raise('Duplicate proxy config definitions for "' + s + '"');
                    }
                    
                    if (in targetProto.config) {
                        Ext.raise('Config "' + s + '" already defined for class ' +
                                  targetProto.$className);
                    }
                    //</debug>
 
                    add[s] = undefined; // sentinel initial value to avoid smashing
                    proxiedConfigs.push(cfg);
 
                    if (!targetProto[name = cfg.names.get]) {
                        targetProto[name] = ConfigProxy.wrapGet(itemGetter, name);
                    }
                    //<debug>
                    else {
                        Ext.raise('Cannot proxy "' + s + '" config getter');
                    }
                    //</debug>
 
                    if (!targetProto[name = cfg.names.set]) {
                        targetProto[name] = ConfigProxy.wrapSet(itemGetter, name, s);
                    }
                    //<debug>
                    else {
                        Ext.raise('Cannot proxy "' + s + '" config setter');
                    }
                    //</debug>
                }
            }
 
            targetClass.addConfig(add);
        },
 
        wrapFn: function(itemGetter, name) {
            return function() {
                var item = this[itemGetter]();
 
                return item && item[name].apply(item, arguments);
            };
        },
 
        wrapGet: function(itemGetter, configGetter) {
            return function() {
                var item = this[itemGetter]();
 
                return item && item[configGetter]();
            };
        },
 
        wrapSet: function(itemGetter, configSetter, itemName) {
            return function(value) {
                var me = this,
                    item, proxiedConfigs;
 
                // We define the proxied configs with "undefined" value so that we can
                // detect this and not smash them by default.
                if (!me.isConfiguring || value !== undefined) {
                    // If the item's applier called mergeProxiedConfigs or getProxiedConfigs
                    // then each config is marked as processed in proxiedConfigs (only during
                    // initialization).
                    item = me[itemGetter]();
 
                    proxiedConfigs = me.$proxiedConfigs; // lazy created by itemGetter
                    
                    if (proxiedConfigs && proxiedConfigs[itemName]) {
                        delete proxiedConfigs[itemName]; // drop only the first set call
                        item = null;
                    }
 
                    if (item) {
                        item[configSetter](value);
                    }
                }
 
                return me;
            };
        }
    }
};
});