/**
 * @class Ext.Object
 *
 * A collection of useful static methods to deal with objects.
 *
 * @singleton
 */
 
(function() {
// The "constructor" for chain:
var TemplateClass = function() {},
    queryRe = /^\?/,
    keyRe = /(\[):?([^\]]*)\]/g,
    nameRe = /^([^\[]+)/, // eslint-disable-line no-useless-escape
    plusRe = /\+/g,
    ExtObject;
 
// @define Ext.lang.Object
// @define Ext.Object
// @require Ext
// @require Ext.lang.Date
ExtObject = Ext.Object = {
    /**
     * @method
     * Returns a new object with the given object as the prototype chain. This method is
     * designed to mimic the ECMA standard `Object.create` method and is assigned to that
     * function when it is available.
     * 
     * **NOTE** This method does not support the property definitions capability of the
     * `Object.create` method. Only the first argument is supported.
     * 
     * @param {Object} object The prototype chain for the new object.
     */
    chain: Object.create || function(object) {
        var result;
 
        TemplateClass.prototype = object;
        result = new TemplateClass();
        TemplateClass.prototype = null;
 
        return result;
    },
 
    /**
     * This method removes all keys from the given object.
     * @param {Object} object The object from which to remove all keys.
     * @return {Object} The given object.
     */
    clear: function(object) {
        var key;
 
        // Safe to delete during iteration
        for (key in object) {
            delete object[key];
        }
 
        return object;
    },
 
    /**
     * Freezes the given object making it immutable. This operation is by default shallow
     * and does not effect objects referenced by the given object.
     * 
     * @method
     * @param {Object} obj The object to freeze.
     * @param {Boolean} [deep=false] Pass `true` to freeze sub-objects recursively.
     * @return {Object} The given object `obj`.
     */
    freeze: Object.freeze
        ? function(obj, deep) {
            var name;
 
            if (obj && typeof obj === 'object' && !Object.isFrozen(obj)) {
                Object.freeze(obj);
 
                if (deep) {
                    for (name in obj) {
                        ExtObject.freeze(obj[name], deep);
                    }
                }
            }
 
            return obj;
        }
        : Ext.identityFn,
 
    /**
     * Converts a `name` - `value` pair to an array of objects with support for nested structures.
     * Useful to construct query strings. For example:
     *
     *     var objects = Ext.Object.toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']);
     *
     *     // objects then equals:
     *     [
     *         { name: 'hobbies', value: 'reading' },
     *         { name: 'hobbies', value: 'cooking' },
     *         { name: 'hobbies', value: 'swimming' },
     *     ];
     *
     *     var objects = Ext.Object.toQueryObjects('dateOfBirth', {
     *         day: 3,
     *         month: 8,
     *         year: 1987,
     *         extra: {
     *             hour: 4
     *             minute: 30
     *         }
     *     }, true); // Recursive
     *
     *     // objects then equals:
     *     [
     *         { name: 'dateOfBirth[day]', value: 3 },
     *         { name: 'dateOfBirth[month]', value: 8 },
     *         { name: 'dateOfBirth[year]', value: 1987 },
     *         { name: 'dateOfBirth[extra][hour]', value: 4 },
     *         { name: 'dateOfBirth[extra][minute]', value: 30 },
     *     ];
     *
     * @param {String} name 
     * @param {Object/Array} value
     * @param {Boolean} [recursive=false] True to traverse object recursively
     * @return {Object[]} 
     */
    toQueryObjects: function(name, value, recursive) {
        var self = ExtObject.toQueryObjects,
            objects = [],
            i, ln;
 
        if (Ext.isArray(value)) {
            for (= 0, ln = value.length; i < ln; i++) {
                if (recursive) {
                    objects = objects.concat(self(name + '[' + i + ']', value[i], true));
                }
                else {
                    objects.push({
                        name: name,
                        value: value[i]
                    });
                }
            }
        }
        else if (Ext.isObject(value)) {
            for (in value) {
                if (value.hasOwnProperty(i)) {
                    if (recursive) {
                        objects = objects.concat(self(name + '[' + i + ']', value[i], true));
                    }
                    else {
                        objects.push({
                            name: name,
                            value: value[i]
                        });
                    }
                }
            }
        }
        else {
            objects.push({
                name: name,
                value: value
            });
        }
 
        return objects;
    },
 
    /* eslint-disable max-len */
    /**
     * Takes an object and converts it to an encoded query string.
     *
     * Non-recursive:
     *
     *     Ext.Object.toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2"
     *     Ext.Object.toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2"
     *     Ext.Object.toQueryString({'some price': '$300'}); // returns "some%20price=%24300"
     *     Ext.Object.toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22"
     *     Ext.Object.toQueryString({colors: ['red', 'green', 'blue']}); // returns "colors=red&colors=green&colors=blue"
     *
     * Recursive:
     *
     *     Ext.Object.toQueryString({
     *         username: 'Jacky',
     *         dateOfBirth: {
     *             day: 1,
     *             month: 2,
     *             year: 1911
     *         },
     *         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
     *     }, true); // returns the following string (broken down and url-decoded for ease of reading purpose):
     *     // username=Jacky
     *     //    &dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911
     *     //    &hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff
     *
     * @param {Object} object The object to encode
     * @param {Boolean} [recursive=false] Whether or not to interpret the object in recursive format.
     * (PHP / Ruby on Rails servers and similar).
     * @return {String} queryString
     */
    toQueryString: function(object, recursive) {
        var paramObjects = [],
            params = [],
            i, j, ln, paramObject, value;
 
        for (in object) {
            if (object.hasOwnProperty(i)) {
                paramObjects = paramObjects.concat(ExtObject.toQueryObjects(i, object[i], recursive));
            }
        }
 
        for (= 0, ln = paramObjects.length; j < ln; j++) {
            paramObject = paramObjects[j];
            value = paramObject.value;
 
            if (Ext.isEmpty(value)) {
                value = '';
            }
            else if (Ext.isDate(value)) {
                value = Ext.Date.toString(value);
            }
 
            params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
        }
 
        return params.join('&');
    },
 
    /**
     * Converts a query string back into an object.
     *
     * Non-recursive:
     *
     *     Ext.Object.fromQueryString("foo=1&bar=2"); // returns {foo: '1', bar: '2'}
     *     Ext.Object.fromQueryString("foo=&bar=2"); // returns {foo: '', bar: '2'}
     *     Ext.Object.fromQueryString("some%20price=%24300"); // returns {'some price': '$300'}
     *     Ext.Object.fromQueryString("colors=red&colors=green&colors=blue"); // returns {colors: ['red', 'green', 'blue']}
     *
     * Recursive:
     *
     *     Ext.Object.fromQueryString(
     *         "username=Jacky&"+
     *         "dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&"+
     *         "hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&"+
     *         "hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
     *
     *     // returns
     *     {
     *         username: 'Jacky',
     *         dateOfBirth: {
     *             day: '1',
     *             month: '2',
     *             year: '1911'
     *         },
     *         hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
     *     }
     *
     * @param {String} queryString The query string to decode
     * @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
     * PHP / Ruby on Rails servers and similar.
     * @return {Object} 
     */
    fromQueryString: function(queryString, recursive) {
        var parts = queryString.replace(queryRe, '').split('&'),
            object = {},
            temp, components, name, value, i, ln,
            part, j, subLn, matchedKeys, matchedName,
            keys, key, nextKey;
 
        for (= 0, ln = parts.length; i < ln; i++) {
            part = parts[i];
 
            if (part.length > 0) {
                components = part.split('=');
                name = components[0];
                name = name.replace(plusRe, '%20');
                name = decodeURIComponent(name);
 
                value = components[1];
 
                if (value !== undefined) {
                    value = value.replace(plusRe, '%20');
                    value = decodeURIComponent(value);
                }
                else {
                    value = '';
                }
 
                if (!recursive) {
                    if (object.hasOwnProperty(name)) {
                        if (!Ext.isArray(object[name])) {
                            object[name] = [object[name]];
                        }
 
                        object[name].push(value);
                    }
                    else {
                        object[name] = value;
                    }
                }
                else {
                    matchedKeys = name.match(keyRe);
                    matchedName = name.match(nameRe);
 
                    //<debug>
                    if (!matchedName) {
                        throw new Error('[Ext.Object.fromQueryString] Malformed query string given, failed parsing name from "' + part + '"');
                    }
                    //</debug>
 
                    name = matchedName[0];
                    keys = [];
 
                    if (matchedKeys === null) {
                        object[name] = value;
                        continue;
                    }
 
                    for (= 0, subLn = matchedKeys.length; j < subLn; j++) {
                        key = matchedKeys[j];
                        key = (key.length === 2) ? '' : key.substring(1, key.length - 1);
                        keys.push(key);
                    }
 
                    keys.unshift(name);
 
                    temp = object;
 
                    for (= 0, subLn = keys.length; j < subLn; j++) {
                        key = keys[j];
 
                        if (=== subLn - 1) {
                            if (Ext.isArray(temp) && key === '') {
                                temp.push(value);
                            }
                            else {
                                temp[key] = value;
                            }
                        }
                        else {
                            if (temp[key] === undefined || typeof temp[key] === 'string') {
                                nextKey = keys[+ 1];
 
                                temp[key] = (Ext.isNumeric(nextKey) || nextKey === '') ? [] : {};
                            }
 
                            temp = temp[key];
                        }
                    }
                }
            }
        }
 
        return object;
    },
    /* eslint-enable max-len */
 
    /**
     * Iterates through an object and invokes the given callback function for each iteration.
     * The iteration can be stopped by returning `false` in the callback function. For example:
     *
     *     var person = {
     *         name: 'Jacky'
     *         hairColor: 'black'
     *         loves: ['food', 'sleeping', 'wife']
     *     };
     *
     *     Ext.Object.each(person, function(key, value, myself) {
     *         console.log(key + ":" + value);
     *
     *         if (key === 'hairColor') {
     *             return false; // stop the iteration
     *         }
     *     });
     *
     * @param {Object} object The object to iterate
     * @param {Function} fn The callback function.
     * @param {String} fn.key 
     * @param {Object} fn.value 
     * @param {Object} fn.object The object itself
     * @param {Object} [scope] The execution scope (`this`) of the callback function
     */
    each: function(object, fn, scope) {
        var enumerables = Ext.enumerables,
            i, property;
 
        if (object) {
            scope = scope || object;
 
            for (property in object) {
                if (object.hasOwnProperty(property)) {
                    if (fn.call(scope, property, object[property], object) === false) {
                        return;
                    }
                }
            }
 
            if (enumerables) {
                for (= enumerables.length; i--;) {
                    if (object.hasOwnProperty(property = enumerables[i])) {
                        if (fn.call(scope, property, object[property], object) === false) {
                            return;
                        }
                    }
                }
            }
        }
    },
 
    /**
     * Iterates through an object and invokes the given callback function for each iteration.
     * The iteration can be stopped by returning `false` in the callback function. For example:
     *
     *     var items = {
     *         1: 'Hello',
     *         2: 'World'
     *     };
     *
     *     Ext.Object.eachValue(items, function(value) {
     *         console.log("Value: " + value);
     *     });
     *
     * This will log 'Hello' and 'World' in no particular order. This method is useful
     * in cases where the keys are not important to the processing, just the values.
     *
     * @param {Object} object The object to iterate
     * @param {Function} fn The callback function.
     * @param {Object} fn.value The value of
     * @param {Object} [scope] The execution scope (`this`) of the callback function
     */
    eachValue: function(object, fn, scope) {
        var enumerables = Ext.enumerables,
            i, property;
 
        scope = scope || object;
 
        for (property in object) {
            if (object.hasOwnProperty(property)) {
                if (fn.call(scope, object[property]) === false) {
                    return;
                }
            }
        }
 
        if (enumerables) {
            for (= enumerables.length; i--;) {
                if (object.hasOwnProperty(property = enumerables[i])) {
                    if (fn.call(scope, object[property]) === false) {
                        return;
                    }
                }
            }
        }
    },
 
    /**
     * Merges any number of objects recursively without referencing them or their children.
     * Note: It will reference arrays if they are only present in one of the objects being merged.
     *
     *     var extjs = {
     *         companyName: 'Ext JS',
     *         products: ['Ext JS', 'Ext GWT', 'Ext Designer'],
     *         isSuperCool: true,
     *         office: {
     *             size: 2000,
     *             location: 'Palo Alto',
     *             isFun: true
     *         }
     *     };
     *
     *     var newStuff = {
     *         companyName: 'Sencha Inc.',
     *         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
     *         office: {
     *             size: 40000,
     *             location: 'Redwood City'
     *         }
     *     };
     *
     *     var sencha = Ext.Object.merge(extjs, newStuff);
     *
     *     // extjs and sencha then equals to
     *     {
     *         companyName: 'Sencha Inc.',
     *         products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
     *         isSuperCool: true,
     *         office: {
     *             size: 40000,
     *             location: 'Redwood City',
     *             isFun: true
     *         }
     *     }
     *
     * @param {Object} destination The object into which all subsequent objects are merged.
     * @param {Object...} object Any number of objects to merge into the destination.
     * @return {Object} merged The destination object with all passed objects merged in.
     */
    merge: function(destination) {
        var i = 1,
            args = arguments,
            ln = args.length,
            mergeFn = ExtObject.merge,
            cloneFn = Ext.clone,
            object, key, value, sourceKey;
 
        for (; i < ln; i++) {
            object = args[i];
 
            for (key in object) {
                value = object[key];
 
                if (value && value.constructor === Object) {
                    sourceKey = destination[key];
 
                    if (sourceKey && sourceKey.constructor === Object) {
                        mergeFn(sourceKey, value);
                    }
                    else {
                        destination[key] = cloneFn(value);
                    }
                }
                else {
                    destination[key] = value;
                }
            }
        }
 
        return destination;
    },
 
    /**
     * @private
     * @param destination
     */
    mergeIf: function(destination) {
        var i = 1,
            ln = arguments.length,
            cloneFn = Ext.clone,
            object, key, value;
 
        for (; i < ln; i++) {
            object = arguments[i];
 
            for (key in object) {
                if (!(key in destination)) {
                    value = object[key];
 
                    if (value && value.constructor === Object) {
                        destination[key] = cloneFn(value);
                    }
                    else {
                        destination[key] = value;
                    }
                }
            }
        }
 
        return destination;
    },
 
    /**
     * Returns all keys of the given object as an array.
     *
     * @param {Object} object 
     * @return {String[]} An array of keys from the object or any of its prototypes.
     * @method
     */
    getAllKeys: function(object) {
        var keys = [],
            property;
 
        for (property in object) {
            keys.push(property);
        }
 
        return keys;
    },
 
    /**
     * Returns the first matching key corresponding to the given value.
     * If no matching value is found, null is returned.
     *
     *     var person = {
     *         name: 'Jacky',
     *         loves: 'food'
     *     };
     *
     *     alert(Ext.Object.getKey(person, 'food')); // alerts 'loves'
     *
     * @param {Object} object 
     * @param {Object} value The value to find
     */
    getKey: function(object, value) {
        var property;
 
        for (property in object) {
            if (object.hasOwnProperty(property) && object[property] === value) {
                return property;
            }
        }
 
        return null;
    },
 
    /**
     * Gets all values of the given object as an array.
     *
     *     var values = Ext.Object.getValues({
     *         name: 'Jacky',
     *         loves: 'food'
     *     }); // ['Jacky', 'food']
     *
     * @param {Object} object 
     * @return {Array} An array of values from the object
     */
    getValues: function(object) {
        var values = [],
            property;
 
        for (property in object) {
            if (object.hasOwnProperty(property)) {
                values.push(object[property]);
            }
        }
 
        return values;
    },
 
    /**
     * Returns the `hasOwnProperty` keys of the given object as an array.
     *
     *     var values = Ext.Object.getKeys({
     *         name: 'Jacky',
     *         loves: 'food'
     *     }); // ['name', 'loves']
     *
     * @param {Object} object 
     * @return {String[]} An array of keys from the object
     * @method
     */
    getKeys: (typeof Object.keys === 'function')
        ? function(object) {
            if (!object) {
                return [];
            }
 
            return Object.keys(object);
        }
        : function(object) {
            var keys = [],
                property;
 
            for (property in object) {
                if (object.hasOwnProperty(property)) {
                    keys.push(property);
                }
            }
 
            return keys;
        },
 
    /**
     * Gets the total number of this object's own properties
     *
     *     var size = Ext.Object.getSize({
     *         name: 'Jacky',
     *         loves: 'food'
     *     }); // size equals 2
     *
     * @param {Object} object 
     * @return {Number} size
     */
    getSize: function(object) {
        var size = 0,
            property;
 
        for (property in object) {
            if (object.hasOwnProperty(property)) {
                size++;
            }
        }
 
        return size;
    },
 
    /**
     * Checks if there are any properties on this object.
     * @param {Object} object 
     * @return {Boolean} `true` if there no properties on the object.
     */
    isEmpty: function(object) {
        var key;
 
        for (key in object) {
            if (object.hasOwnProperty(key)) {
                return false;
            }
        }
 
        return true;
    },
 
    /**
     * @method
     * Shallow compares the contents of 2 objects using strict equality. Objects are
     * considered equal if they both have the same set of properties and the
     * value for those properties equals the other in the corresponding object.
     * 
     *     // Returns true
     *     Ext.Object.equals({
     *         foo: 1,
     *         bar: 2
     *     }, {
     *         foo: 1,
     *         bar: 2
     *     });
     * 
     * @param {Object} object1 
     * @param {Object} object2 
     * @return {Boolean} `true` if the objects are equal.
     */
    equals: (function() {
        var check = function(o1, o2) {
            var key;
 
            for (key in o1) {
                if (o1.hasOwnProperty(key)) {
                    if (o1[key] !== o2[key]) {
                        return false;
                    }
                }
            }
 
            return true;
        };
 
        return function(object1, object2) {
            // Short circuit if the same object is passed twice
            if (object1 === object2) {
                return true;
            }
 
            if (object1 && object2) {
                // Do the second check because we could have extra keys in
                // object2 that don't exist in object1.
                return check(object1, object2) && check(object2, object1);
            }
            else if (!object1 && !object2) {
                return object1 === object2;
            }
            else {
                return false;
            }
        };
    })(),
 
    /**
     * @private
     */
    fork: function(obj) {
        var ret, key, value;
 
        if (obj && obj.constructor === Object) {
            ret = ExtObject.chain(obj);
 
            for (key in obj) {
                value = obj[key];
 
                if (value) {
                    if (value.constructor === Object) {
                        ret[key] = ExtObject.fork(value);
                    }
                    else if (value instanceof Array) {
                        ret[key] = Ext.Array.clone(value);
                    }
                }
            }
        }
        else {
            ret = obj;
        }
 
        return ret;
    },
 
    defineProperty: ('defineProperty' in Object)
        ? Object.defineProperty
        : function(object, name, descriptor) {
            if (!Object.prototype.__defineGetter__) {
                return;
            }
 
            if (descriptor.get) {
                object.__defineGetter__(name, descriptor.get);
            }
 
            if (descriptor.set) {
                object.__defineSetter__(name, descriptor.set);
            }
        },
 
    /**
     * @private
     */
    classify: function(object) {
        var prototype = object,
            objectProperties = [],
            propertyClassesMap = {},
            objectClass, key, value;
 
        objectClass = function() {
            var property, i, ln;
 
            for (= 0, ln = objectProperties.length; i < ln; i++) {
                property = objectProperties[i];
                this[property] = new propertyClassesMap[property]();
            }
        };
 
        for (key in object) {
            if (object.hasOwnProperty(key)) {
                value = object[key];
 
                if (value && value.constructor === Object) {
                    objectProperties.push(key);
                    propertyClassesMap[key] = ExtObject.classify(value);
                }
            }
        }
 
        objectClass.prototype = prototype;
 
        return objectClass;
    }
};
 
/**
 * A convenient alias method for {@link Ext.Object#merge}.
 *
 * @member Ext
 * @method merge
 * @inheritdoc Ext.Object#merge
 */
Ext.merge = Ext.Object.merge;
 
/**
 * @private
 * @member Ext
 */
Ext.mergeIf = Ext.Object.mergeIf;
 
}());