/**
 * @class Ext.Config
 * This class manages a config property. Instances of this type are created and cached as
 * classes declare their config properties. One instance of this class is created per
 * config property name.
 *
 *      Ext.define('MyClass', {
 *          config: {
 *              foo: 42
 *          }
 *      });
 *
 * This uses the cached `Ext.Config` instance for the "foo" property.
 *
 * When config properties apply options to config properties a prototype chained object is
 * created from the cached instance. For example:
 *
 *      Ext.define('MyClass', {
 *          config: {
 *              foo: {
 *                  $value: 42,
 *                  lazy: true
 *              }
 *          }
 *      });
 *
 * This creates a prototype chain to the cached "foo" instance of `Ext.Config` and applies
 * the `lazy` option to that new instance. This chained instance is then kept by the
 * `Ext.Configurator` for that class.
 * @private
 */
Ext.Config = function(name) {
// @define Ext.class.Config
// @define Ext.Config
 
    var me = this,
        capitalizedName = name.charAt(0).toUpperCase() + name.substr(1);
 
    /**
     * @property {String} name
     * The name of this config property.
     * @readonly
     * @private
     * @since 5.0.0
     */
    me.name = name;
 
    /**
     * @property {Object} names
     * This object holds the cached names used to lookup properties or methods for this
     * config property. The properties of this object are explained in the context of an
     * example property named "foo".
     *
     * @property {String} names.internal The default backing property ("_foo").
     *
     * @property {String} names.initializing The property that is `true` when the config
     * is being initialized ("isFooInitializing").
     *
     * @property {String} names.apply The name of the applier method ("applyFoo").
     *
     * @property {String} names.update  The name of the updater method ("updateFoo").
     *
     * @property {String} names.get The name of the getter method ("getFoo").
     *
     * @property {String} names.set The name of the setter method ("setFoo").
     *
     * @property {String} names.initGet The name of the initializing getter ("initGetFoo").
     *
     * @property {String} names.changeEvent The name of the change event ("foochange").
     *
     * @readonly
     * @private
     * @since 5.0.0
     */
    me.names = {
        internal: '_' + name,
        initializing: 'is' + capitalizedName + 'Initializing',
        apply: 'apply' + capitalizedName,
        update: 'update' + capitalizedName,
        get: 'get' + capitalizedName,
        set: 'set' + capitalizedName,
        initGet: 'initGet' + capitalizedName,
        changeEvent: name.toLowerCase() + 'change'
    };
 
    // This allows folks to prototype chain on top of these objects and yet still cache
    // generated methods at the bottom of the chain.
    me.root = me;
};
 
Ext.Config.map = {};
 
Ext.Config.get = function(name) {
    var map = Ext.Config.map,
        ret = map[name] || (map[name] = new Ext.Config(name));
 
    return ret;
};
 
Ext.Config.prototype = {
    self: Ext.Config,
 
    isConfig: true,
 
    /**
     * @cfg {Boolean} [cached=false]
     * When set as `true` the config property will be stored on the class prototype once
     * the first instance has had a chance to process the default value.
     * @private
     * @since 5.0.0
     */
 
    /**
     * @cfg {Boolean} [lazy=false]
     * When set as `true` the config property will not be immediately initialized during
     * the `initConfig` call.
     * @private
     * @since 5.0.0
     */
 
    /**
     * @cfg {Boolean} [evented=false]
     * When set as `true` the config property will be treated as a
     * {@link Ext.Evented Evented Config}.
     * @private
     * @since 6.0.0
     */
 
    /**
     * @cfg {Function} [merge]
     * This function if supplied will be called as classes or instances provide values
     * that need to be combined with inherited values. The function should return the
     * value that will be the config value. Further calls may receive such returned
     * values as `oldValue`.
     *
     * @cfg {Mixed} merge.newValue The new value to merge with the old.
     *
     * @cfg {Mixed} merge.oldValue The current value prior to `newValue` being merged.
     *
     * @cfg {Mixed} merge.target The class or instance to which the merged config value
     * will be applied.
     *
     * @cfg {Ext.Class} merge.mixinClass The mixin providing the `newValue` or `null` if
     * the `newValue` is not being provided by a mixin.
     */
 
    combine: function(value, baseValue, instance, clone) {
        var cfg = this;
 
        if (cfg.merge) {
            value = cfg.merge(clone ? Ext.clone(value) : value, baseValue, instance);
        }
        else if (value && value.constructor === Object &&
                 baseValue && baseValue.constructor === Object) {
            value = Ext.merge({}, baseValue, value);
        }
        else if (clone && value) {
            value = Ext.clone(value);
        }
 
        return value;
    },
 
    equals: function(value1, value2) {
        return value1 === value2;
    },
 
    getGetter: function() {
        return this.getter || (this.root.getter = this.makeGetter());
    },
    
    getInitGetter: function() {
        return this.initGetter || (this.root.initGetter = this.makeInitGetter());
    },
 
    getSetter: function() {
        return this.setter || (this.root.setter = this.makeSetter());
    },
 
    getEventedSetter: function() {
        return this.eventedSetter || (this.root.eventedSetter = this.makeEventedSetter());
    },
 
    /**
     * Returns the name of the property that stores this config on the given instance or
     * class prototype.
     * @param {Object} target 
     * @return {String} 
     */
    getInternalName: function(target) {
        return target.$configPrefixed ? this.names.internal : this.name;
    },
 
    mergeNew: function(newValue, oldValue, target, mixinClass) {
        var ret, key;
 
        if (!oldValue) {
            ret = newValue;
        }
        else if (!newValue) {
            ret = oldValue;
        }
        else {
            ret = Ext.Object.chain(oldValue);
 
            for (key in newValue) {
                if (!mixinClass || !(key in ret)) {
                    ret[key] = newValue[key];
                }
            }
        }
        
        return ret;
    },
 
    /**
     * Merges the `newValue` and the `oldValue` assuming that these are basically objects
     * the represent sets. For example something like:
     *
     *      {
     *          foo: true,
     *          bar: true
     *      }
     *
     * The merge process converts arrays like the following into the above:
     *
     *      [ 'foo', 'bar' ]
     *
     * @param {String/String[]/Object} newValue
     * @param {Object} oldValue 
     * @param {Boolean} [preserveExisting=false] 
     * @return {Object} 
     * @private
     * @since 5.0.0
     */
    mergeSets: function(newValue, oldValue, preserveExisting) {
        var ret = oldValue ? Ext.Object.chain(oldValue) : {},
            i, val;
 
        if (newValue instanceof Array) {
            for (= newValue.length; i--;) {
                val = newValue[i];
                
                if (!preserveExisting || !(val in ret)) {
                    ret[val] = true;
                }
            }
        }
        else if (newValue) {
            if (newValue.constructor === Object) {
                for (in newValue) {
                    val = newValue[i];
                    
                    if (!preserveExisting || !(in ret)) {
                        ret[i] = val;
                    }
                }
            }
            else if (!preserveExisting || !(newValue in ret)) {
                ret[newValue] = true;
            }
        }
 
        return ret;
    },
 
    //--------------------------------------------------
    // Factories
 
    makeGetter: function() {
        var name = this.name,
            prefixedName = this.names.internal;
 
        return function() {
            var internalName = this.$configPrefixed ? prefixedName : name;
            
            return this[internalName];
        };
    },
 
    makeInitGetter: function() {
        var name = this.name,
            names = this.names,
            setName = names.set,
            getName = names.get,
            initializingName = names.initializing;
 
        return function() {
            var me = this;
 
            me[initializingName] = true;
            // Remove the initGetter from the instance now that the value has been set.
            delete me[getName];
 
            me[setName](me.config[name]);
            delete me[initializingName];
 
            return me[getName].apply(me, arguments);
        };
    },
 
    makeSetter: function() {
        var name = this.name,
            names = this.names,
            prefixedName = names.internal,
            getName = names.get,
            applyName = names.apply,
            updateName = names.update,
            setter;
 
        // http://jsperf.com/method-call-apply-or-direct
        // http://jsperf.com/method-detect-invoke
        setter = function(value) {
            var me = this,
                internalName = me.$configPrefixed ? prefixedName : name,
                oldValue = me[internalName],
                watch;
 
            // Remove the initGetter from the instance now that the value has been set.
            delete me[getName];
 
            if (!me[applyName] || (value = me[applyName](value, oldValue)) !== undefined) {
                // The old value might have been changed at this point
                // (after the apply call chain) so it should be read again
                if (value !== (oldValue = me[internalName])) {
                    me[internalName] = value;
 
                    if (me[updateName]) {
                        me[updateName](value, oldValue);
                    }
 
                    watch = me.$configWatch;
 
                    if (watch && !me.isConfiguring) {
                        // Since the updater could have modified things (tho unlikely), just
                        // re-read the stored value:
                        watch.fire(name, [ me, name, me[internalName], oldValue ]);
                    }
                }
            }
 
            return me;
        };
 
        setter.$isDefault = true;
 
        return setter;
    },
 
    makeEventedSetter: function() {
        var name = this.name,
            names = this.names,
            prefixedName = names.internal,
            getName = names.get,
            applyName = names.apply,
            updateName = names.update,
            changeEventName = names.changeEvent,
            updateFn = function(me, value, oldValue, internalName) {
                me[internalName] = value;
                
                if (me[updateName]) {
                    me[updateName](value, oldValue);
                }
 
                // eslint-disable-next-line vars-on-top
                var watch = me.$configWatch;
 
                if (watch) { // !me.isConfiguring is assured
                    watch.fire(name, [ me, name, value, oldValue ]);
                }
            },
            setter;
 
        // http://jsperf.com/method-call-apply-or-direct
        // http://jsperf.com/method-detect-invoke
        setter = function(value) {
            var me = this,
                internalName = me.$configPrefixed ? prefixedName : name,
                oldValue = me[internalName];
 
            // Remove the initGetter from the instance now that the value has been set.
            delete me[getName];
 
            if (!me[applyName] || (value = me[applyName](value, oldValue)) !== undefined) {
                // The old value might have been changed at this point
                // (after the apply call chain) so it should be read again
                if (value !== (oldValue = me[internalName])) {
                    if (me.isConfiguring) {
                        me[internalName] = value;
 
                        if (me[updateName]) {
                            me[updateName](value, oldValue);
                        }
                    }
                    else {
                        me.fireEventedAction(changeEventName, [me, value, oldValue],
                                             updateFn, me, [me, value, oldValue, internalName]);
                    }
                }
            }
 
            return me;
        };
 
        setter.$isDefault = true;
 
        return setter;
    }
};