/**
 * WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage}
 * and {@link Ext.data.proxy.SessionStorage SessionStorage} proxies. It uses the new HTML5
 * key/value client-side storage objects to save {@link Ext.data.Model model instances} for
 * offline use.
 * @private
 */
Ext.define('Ext.data.proxy.WebStorage', {
    extend: 'Ext.data.proxy.Client',
    alternateClassName: 'Ext.data.WebStorageProxy',
    requires: [
        'Ext.data.identifier.Sequential'
    ],
 
    config: {
        /**
         * @cfg {String} id
         * The unique ID used as the key in which all record data are stored in the local
         * storage object.
         */
        id: undefined
    },
 
    /**
     * @cfg {Object} reader
     * Not used by web storage proxy.
     * @hide
     */
 
    /**
     * @cfg {Object} writer
     * Not used by web storage proxy.
     * @hide
     */
 
    /**
     * Creates the proxy, throws an error if local storage is not supported in the current browser.
     * @param {Object} config (optional) Config object.
     */
    constructor: function(config) {
        this.callParent(arguments);
 
        /**
         * @property {Object} cache
         * Cached map of records already retrieved by this Proxy. Ensures that the same instance is
         * always retrieved.
         */
        this.cache = {};
 
        //<debug>
        if (this.getStorageObject() === undefined) {
            Ext.raise("Local Storage is not supported in this browser, please use another type " +
                      "of data proxy");
        }
        //</debug>
 
        //<debug>
        if (this.getId() === undefined) {
            Ext.raise("No unique id was provided to the local storage proxy. " +
                      "See Ext.data.proxy.LocalStorage documentation for details");
        }
        //</debug>
 
        this.initialize();
    },
 
    /**
     * @method create
     * @inheritdoc
     */
    create: function(operation) {
        var me = this,
            records = operation.getRecords(),
            length = records.length,
            ids = me.getIds(),
            id, record, i, identifier;
 
        if (me.isHierarchical === undefined) {
            // if the storage object does not yet contain any data, this is the first point
            // at which we can determine whether or not this proxy deals with hierarchical data.
            // it cannot be determined during initialization because the Model is not decorated
            // with NodeInterface until it is used in a TreeStore
            me.isHierarchical = !!records[0].isNode;
            
            if (me.isHierarchical) {
                me.getStorageObject().setItem(me.getTreeKey(), true);
            }
        }
 
        for (= 0; i < length; i++) {
            record = records[i];
 
            if (record.phantom) {
                identifier = record.identifier;
                
                if (identifier && identifier.isUnique) {
                    id = record.getId();
                }
                else {
                    id = me.getNextId();
                }
            }
            else {
                id = record.getId();
            }
 
            me.setRecord(record, id);
            record.commit();
            ids.push(id);
        }
 
        me.setIds(ids);
 
        operation.setSuccessful(true);
    },
 
    /**
     * @method read
     * @inheritdoc
     */
    read: function(operation) {
        var me = this,
            allRecords,
            records = [],
            success = true,
            Model = me.getModel(),
            validCount = 0,
            recordCreator = operation.getRecordCreator(),
            filters, sorters, limit, filterLen, valid, record, ids, length, data, id, i, j;
 
        if (me.isHierarchical) {
            records = me.getTreeData();
        }
        else {
            ids = me.getIds();
            length = ids.length;
            id = operation.getId();
            
            // read a single record
            if (id) {
                data = me.getRecord(id);
                
                if (data !== null) {
                    record = recordCreator ? recordCreator(data, Model) : new Model(data);
                }
 
                if (record) {
                    records.push(record);
                }
                else {
                    success = false;
                }
            }
            else {
                sorters = operation.getSorters();
                filters = operation.getFilters();
                limit = operation.getLimit();
                allRecords = [];
 
                // build an array of all records first first so we can sort them before
                // applying filters or limit.  These are Model instances instead of raw
                // data objects so that the sorter and filter Fn can use the Model API
                for (= 0; i < length; i++) {
                    data = me.getRecord(ids[i]);
                    record = recordCreator ? recordCreator(data, Model) : new Model(data);
                    allRecords.push(record);
                }
 
                if (sorters) {
                    Ext.Array.sort(allRecords, Ext.util.Sorter.createComparator(sorters));
                }
 
                for (= operation.getStart() || 0; i < length; i++) {
                    record = allRecords[i];
                    valid = true;
 
                    if (filters) {
                        for (= 0, filterLen = filters.length; j < filterLen; j++) {
                            valid = filters[j].filter(record);
                        }
                    }
 
                    if (valid) {
                        records.push(record);
                        validCount++;
                    }
 
                    if (limit && validCount === limit) {
                        break;
                    }
                }
            }
 
        }
 
        if (success) {
            operation.setResultSet(new Ext.data.ResultSet({
                records: records,
                total: records.length,
                loaded: true
            }));
            
            operation.setSuccessful(true);
        }
        else {
            operation.setException('Unable to load records');
        }
    },
 
    /**
     * @method update
     * @inheritdoc
     */
    update: function(operation) {
        var records = operation.getRecords(),
            length = records.length,
            ids = this.getIds(),
            record, id, i;
 
        for (= 0; i < length; i++) {
            record = records[i];
            this.setRecord(record);
            record.commit();
 
            // we need to update the set of ids here because it's possible that
            // a non-phantom record was added to this proxy - in which case the record's
            // id would never have been added via the normal 'create' call
            id = record.getId();
            
            if (id !== undefined && Ext.Array.indexOf(ids, id) === -1) {
                ids.push(id);
            }
        }
        
        this.setIds(ids);
        operation.setSuccessful(true);
    },
 
    /**
     * @method erase
     * @inheritdoc
     */
    erase: function(operation) {
        var me = this,
            records = operation.getRecords(),
            ids = me.getIds(),
            idLength = ids.length,
            newIds = [],
            removedHash = {},
            i = records.length,
            id;
 
        for (; i--;) {
            Ext.apply(removedHash, me.removeRecord(records[i]));
        }
 
        for (= 0; i < idLength; i++) {
            id = ids[i];
            
            if (!removedHash[id]) {
                newIds.push(id);
            }
        }
 
        me.setIds(newIds);
        operation.setSuccessful(true);
    },
 
    /**
     * @private
     * Fetches record data from the Proxy by ID.
     * @param {String} id The record's unique ID
     * @return {Object} The record data
     */
    getRecord: function(id) {
        var me = this,
            cache = me.cache,
            data;
        
        data = !cache[id]
            ? Ext.decode(me.getStorageObject().getItem(me.getRecordKey(id)))
            : cache[id];
 
        if (!data) {
            return null;
        }
 
        cache[id] = data;
        data[me.getModel().prototype.idProperty] = id;
 
        // In order to preserve the cache, we MUST copy it here because
        // Models use the incoming raw data as their data object and convert/default values
        // into that object
        return Ext.merge({}, data);
    },
 
    /**
     * Saves the given record in the Proxy.
     * @param {Ext.data.Model} record The model instance
     * @param {String} [id] The id to save the record under (defaults to the value of the
     * record's getId() function)
     */
    setRecord: function(record, id) {
        if (id) {
            record.set('id', id, {
                commit: true
            });
        }
        else {
            id = record.getId();
        }
 
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            rawData = record.getData(),
            data = {},
            model = me.getModel(),
            fields = model.getFields(),
            length = fields.length,
            i = 0,
            field, name, obj, key, value;
 
        for (; i < length; i++) {
            field = fields[i];
            name = field.name;
 
            if (field.persist) {
                value = rawData[name];
                
                if (field.isDateField && field.dateFormat && Ext.isDate(value)) {
                    value = Ext.Date.format(value, field.dateFormat);
                }
                else if (field.serialize) {
                    value = field.serialize(value, record);
                }
                
                data[name] = value;
            }
        }
 
        // no need to store the id in the data, since it is already stored in the record key
        delete data[model.prototype.idProperty];
 
        // if the record is a tree node and it's a direct child of the root node, do not store
        // the parentId
        if (record.isNode && record.get('depth') === 1) {
            delete data.parentId;
        }
 
        obj = me.getStorageObject();
        key = me.getRecordKey(id);
 
        // keep the cache up to date
        me.cache[id] = data;
 
        // iPad bug requires that we remove the item before setting it
        obj.removeItem(key);
        obj.setItem(key, Ext.encode(data));
    },
 
    /**
     * @private
     * Physically removes a given record from the local storage and recursively removes children
     * if the record is a tree node. Used internally by {@link #destroy}.
     * @param {Ext.data.Model} record The record to remove
     * @return {Object} a hash with the ids of the records that were removed as keys and the
     * records that were removed as values
     */
    removeRecord: function(record) {
        var me = this,
            id = record.getId(),
            records = {},
            i, childNodes;
 
        records[id] = record;
        me.getStorageObject().removeItem(me.getRecordKey(id));
        delete me.cache[id];
 
        if (record.childNodes) {
            childNodes = record.childNodes;
            
            for (= childNodes.length; i--;) {
                Ext.apply(records, me.removeRecord(childNodes[i]));
            }
        }
 
        return records;
    },
 
    /**
     * @private
     * Given the id of a record, returns a unique string based on that id and the id of this proxy.
     * This is used when storing data in the local storage object and should prevent naming
     * collisions.
     * @param {String/Number/Ext.data.Model} id The record id, or a Model instance
     * @return {String} The unique key for this record
     */
    getRecordKey: function(id) {
        if (id.isModel) {
            id = id.getId();
        }
 
        return Ext.String.format("{0}-{1}", this.getId(), id);
    },
 
    /**
     * @private
     * Returns the unique key used to store the current record counter for this proxy. This is used
     * internally when realizing models (creating them when they used to be phantoms), in order to
     * give each model instance a unique id.
     * @return {String} The counter key
     */
    getRecordCounterKey: function() {
        return Ext.String.format("{0}-counter", this.getId());
    },
 
    /**
     * @private
     * Returns the unique key used to store the tree indicator. This is used internally to
     * determine if the stored data is hierarchical
     * @return {String} The counter key
     */
    getTreeKey: function() {
        return Ext.String.format("{0}-tree", this.getId());
    },
 
    /**
     * @private
     * Returns the array of record IDs stored in this Proxy
     * @return {Number[]} The record IDs. Each is cast as a Number
     */
    getIds: function() {
        var me = this,
            ids = (me.getStorageObject().getItem(me.getId()) || "").split(","),
            length = ids.length,
            isString = this.getIdField().isStringField,
            i;
 
        if (length === 1 && ids[0] === "") {
            ids = [];
        }
        else {
            for (= 0; i < length; i++) {
                ids[i] = isString ? ids[i] : +ids[i];
            }
        }
 
        return ids;
    },
    
    getIdField: function() {
        return this.getModel().prototype.idField;
    },
 
    /**
     * @private
     * Saves the array of ids representing the set of all records in the Proxy
     * @param {Number[]} ids The ids to set
     */
    setIds: function(ids) {
        var obj = this.getStorageObject(),
            str = ids.join(","),
            id = this.getId();
 
        obj.removeItem(id);
 
        if (!Ext.isEmpty(str)) {
            obj.setItem(id, str);
        }
    },
 
    /**
     * @private
     * Returns the next numerical ID that can be used when realizing a model instance
     * (see getRecordCounterKey). Increments the counter.
     * @return {Number} The id
     */
    getNextId: function() {
        var me = this,
            obj = me.getStorageObject(),
            key = me.getRecordCounterKey(),
            isString = me.getIdField().isStringField,
            id;
 
        id = me.idGenerator.generate();
 
        obj.setItem(key, id);
 
        if (isString) {
            id = id + '';
        }
 
        return id;
    },
 
    /**
     * Gets tree data and transforms it from key value pairs into a hierarchical structure.
     * @private
     * @return {Ext.data.NodeInterface[]} 
     */
    getTreeData: function() {
        var me = this,
            ids = me.getIds(),
            length = ids.length,
            records = [],
            recordHash = {},
            root = [],
            i = 0,
            Model = me.getModel(),
            idProperty = Model.prototype.idProperty,
            rootLength, record, parent, parentId, children, id;
 
        for (; i < length; i++) {
            id = ids[i];
            // get the record for each id
            record = me.getRecord(id);
            // push the record into the records array
            records.push(record);
            // add the record to the record hash so it can be easily retrieved by id later
            recordHash[id] = record;
            
            if (!record.parentId) {
                // push records that are at the root level (those with no parent id) into the
                // "root" array
                root.push(record);
            }
        }
 
        rootLength = root.length;
 
        // sort the records by parent id for greater efficiency, so that each parent record only
        // has to be found once for all of its children
        Ext.Array.sort(records, me.sortByParentId);
 
        // append each record to its parent, starting after the root node(s), since root nodes
        // do not need to be attached to a parent
        for (= rootLength; i < length; i++) {
            record = records[i];
            parentId = record.parentId;
            
            if (!parent || parent[idProperty] !== parentId) {
                // if this record has a different parent id from the previous record, we need to
                // look up the parent by id.
                parent = recordHash[parentId];
                parent.children = children = [];
            }
 
            // push the record onto its parent's children array
            children.push(record);
        }
 
        for (= length; i--;) {
            record = records[i];
            
            if (!record.children && !record.leaf) {
                // set non-leaf nodes with no children to loaded so the proxy won't try to
                // dynamically load their contents when they are expanded
                record.loaded = true;
            }
        }
 
        // Create model instances out of all the "root-level" nodes.
        for (= rootLength; i--;) {
            record = root[i];
            root[i] = new Model(record);
        }
 
        return root;
    },
 
    /**
     * Sorter function for sorting records by parentId
     * @private
     * @param {Object} node1 
     * @param {Object} node2 
     * @return {Number} 
     */
    sortByParentId: function(node1, node2) {
        return (node1.parentId || 0) - (node2.parentId || 0);
    },
 
    /**
     * @private
     * Sets up the Proxy by claiming the key in the storage object that corresponds to the unique
     * id of this Proxy. Called automatically by the constructor, this should not need to be called
     * again unless {@link #clear} has been called.
     */
    initialize: function() {
        var me = this,
            storageObject = me.getStorageObject(),
            lastId = +storageObject.getItem(me.getRecordCounterKey()),
            id = me.getId();
 
        storageObject.setItem(id, storageObject.getItem(id) || "");
        
        if (storageObject.getItem(me.getTreeKey())) {
            me.isHierarchical = true;
        }
 
        me.idGenerator = new Ext.data.identifier.Sequential({
            seed: lastId ? lastId + 1 : 1
        });
    },
 
    /**
     * Destroys all records stored in the proxy and removes all keys and values used to support
     * the proxy from the storage object.
     */
    clear: function() {
        var me = this,
            obj = me.getStorageObject(),
            ids = me.getIds(),
            len = ids.length,
            i;
 
        // remove all the records
        for (= 0; i < len; i++) {
            obj.removeItem(me.getRecordKey(ids[i]));
        }
 
        // remove the supporting objects
        obj.removeItem(me.getRecordCounterKey());
        obj.removeItem(me.getTreeKey());
        obj.removeItem(me.getId());
 
        // clear the cache
        me.cache = {};
    },
 
    /**
     * @private
     * Abstract function which should return the storage object that data will be saved to.
     * This must be implemented in each subclass.
     * @return {Object} The storage object
     */
    getStorageObject: function() {
        //<debug>
        Ext.raise("The getStorageObject function has not been defined in your " +
                  "Ext.data.proxy.WebStorage subclass");
        //</debug>
    }
});