/**
 * This class provides a common API to LocalStorage with backwards compatibility for IE.
 * 
 * The primary aspects of this API match the HTML5 standard API except that this class
 * provides a scoping mechanism to isolate property values by instance. This scope is
 * determined from the `id` property. Further, this class does not expose the number of
 * keys in the store as a `length` property as this cannot be maintained reliably without
 * undue cost. Instead there is a `getKeys` method that returns the cached array of keys
 * which is lazily populated on first call.
 * 
 * For example:
 * 
 *      var store = new Ext.util.LocalStorage({
 *              id: 'foo'
 *          });
 * 
 *      store.setItem('bar', 'stuff');
 *      
 *      // Equivalent to:
 *      window.localStorage.setItem('foo-bar', 'stuff');
 * 
 * In all cases, the `id` property is only used by the underlying storage and should not
 * be needed in item access calls or appear when enumerating keys.
 * 
 * To continue with the previous example:
 * 
 *      var keys = store.getKeys();
 *      console.log(keys.length);   // logs 1
 *      console.log(store.key(0));  // logs "bar"
 *
 * ## Sharing Instances
 * 
 * The management of the underlying storage can be broken if multiple instances of this
 * class are created with the same `id` simultaneously. To avoid creating multiple instances
 * with the same `id`, use the `get` method and it will lazily create and share a single
 * instance. When you are done with the shared instance, call `release`.
 * 
 *      var storage = Ext.util.LocalStorage.get('id');
 *      
 *      ...
 *      
 *      storage.release(); // do not call `destroy` as others may be using this object
 *
 * **IMPORTANT:** Do not mix direction instantiation and `get` with the same `id`.
 * 
 * ## Legacy IE
 * 
 * Older IE browsers (specifically IE7 and below) do not support `localStorage` so this
 * class provides equivalent support using the IE proprietary persistence mechanism: the
 * [`userData` behavior](http://msdn.microsoft.com/en-us/library/ms531424(VS.85).aspx). In
 * this mode, the `id` serves as name passed to the `load` and `save` methods and as the
 * suffix on the DOM element added to the `head`.
 * 
 * In this mode, writes to the underlying storage are buffered and delayed for performance
 * reasons. This can be managed using the `flushDelay` config or by directly calling the
 * `save` method.
 *
 * @since 4.2.2
 */
Ext.define('Ext.util.LocalStorage', {
    /**
     * The unique identifier for this store. This config is required to scope this storage
     * distinctly from others. Ultimately, this is used to set a prefix on all keys.
     * @cfg {String} id
     */
    id: null,
 
    /**
     * @property {Boolean} destroyed
     * This property is set to `true` when the instance's `destroy` method is called.
     * @readonly
     */
    destroyed: false,
 
    /**
     * @cfg {Boolean} lazyKeys
     * Determines if the keys collection is continuously maintained by this object. By
     * default the keys array is lazily fetched from the underlying store and when keys
     * are removed, the array is discarded. This heuristic tends to be safer than doing
     * the linear removal and array rippling to remove keys from the array on each call
     * to `removeItem`. If the cost of scanning `localStorage` for keys is high enough
     * and if the keys are frequently needed, then this flag can be set to `false` to
     * instruct this class to maintain the keys array once it has been determined.
     */
    lazyKeys: true,
 
    /**
     * @cfg {String} prefix
     * The prefix to apply to all `localStorage` keys manages by this instance. This does
     * not apply to the legacy IE mechanism but only to the HTML5 `localStorage` keys. If
     * not provided, the `id` property initializes this value with `"id-"`.
     */
    prefix: '',
 
    /**
     * @cfg {Boolean} session
     * Specify this as `true` to use `sessionStorage` instead of the default `localStoreage`.
     * This option is not supported in legacy IE browsers (IE 6 and 7) and is ignored.
     */
    session: false,
 
    /**
     * @property {String[]} _keys
     * The array of all key names. This will be `null` if the keys need to be redetermined
     * by the `getKeys` method.
     * @private
     * @readonly
     */
    _keys: null,
 
    /**
     * @property _store
     * The Storage instance used to store items. This is based on the `session` config.
     * @private
     * @readonly
     */
    _store: null,
 
    /**
     * @property {Number} _users
     * The number of users that have requested this instance using the `get` method and
     * have not yet called `release`.
     * @private
     * @readonly
     */
    _users: 0,
 
    statics: {
        cache: {},
 
        /**
         * Returns a shared instance of the desired local store given its `id`. When you
         * are finished with the returned object call the `release` method:
         * 
         *      var store = Ext.util.LocalStorage.get('foo');
         *      
         *      // .. use store
         *      
         *      store.release();
         * 
         * **NOTE:** Do not mix this call with direct instantiation of the same `id`.
         * @param {String/Object} id The `id` of the desired instance or a config object
         * with an `id` property at a minimum.
         * @return {Ext.util.LocalStorage} The desired instance, created if needed.
         */
        get: function(id) {
            var me = this,
                cache = me.cache,
                config = {
                    _users: 1 // allow constructor to recognize us as the caller
                },
                instance;
 
            if (Ext.isString(id)) {
                config.id = id;
            }
            else {
                Ext.apply(config, id);
            }
 
            if (!(instance = cache[config.id])) {
                instance = new me(config);
            }
            else {
                //<debug>
                if (instance === true) {
                    Ext.raise('Creating a shared instance of private local store "' +
                        me.id + '".');
                }
                //</debug>
 
                ++instance._users;
            }
 
            return instance;
        },
 
        /**
         * This will be `true` if some form of local storage is supported or `false` if not.
         * @property {Boolean} supported
         * @readonly
         */
        supported: true
    },
 
    constructor: function(config) {
        var me = this;
 
        Ext.apply(me, config);
 
        //<debug>
        if (!me.hasOwnProperty('id')) {
            Ext.raise("No id was provided to the local store.");
        }
        //</debug>
 
        if (me._users) {
            // When we are created by get() for shared use, the _users property is set to
            // 1... so this means we are being created for shared use: add this instance
            // to the cache.
            Ext.util.LocalStorage.cache[me.id] = me;
        }
        //<debug>
        else {
            // else we are being created directly so check that this id is not also in the
            // cache ... that would be "extraordinarily bad".
 
            if (Ext.util.LocalStorage.cache[me.id]) {
                Ext.raise('Cannot create duplicate instance of local store "' +
                    me.id + '". Use Ext.util.LocalStorage.get() to share instances.');
            }
 
            // We put true in the cache to be detectable by get() for diagnostic reasons
            // but no "this" to avoid leaking the store:
            Ext.util.LocalStorage.cache[me.id] = true;
        }
        //</debug>
 
        me.init();
    },
 
    /**
     * Initializes this instance.
     * @private
     */
    init: function() {
        var me = this,
            id = me.id;
 
        if (!me.prefix && id) {
            me.prefix = id + '-';
        }
 
        me._store = (me.session ? window.sessionStorage : window.localStorage);
    },
 
    /**
     * Destroys this instance and for legacy IE, ensures data is flushed to persistent
     * storage. This method should not be called directly on instances returned by the
     * `get` method. Call `release` instead for such instances.
     * 
     * *NOTE:* For non-legacy IE browsers, there is no harm in failing to call this
     * method. In legacy IE, however, failing to call this method can result in memory
     * leaks.
     */
    destroy: function() {
        var me = this;
 
        //<debug>
        if (me._users) {
            Ext.log.warn('LocalStorage(id=' + me.id + ') destroyed while in use');
        }
        //</debug>
 
        delete Ext.util.LocalStorage.cache[me.id];
        me._store = me._keys = null;
        me.callParent();
    },
 
    /**
     * Returns the keys for this storage.
     * @return {String[]} The keys for this storage. This array should be considered as
     * readonly.
     */
    getKeys: function() {
        var me = this,
            store = me._store,
            prefix = me.prefix,
            keys = me._keys,
            n = prefix.length,
            i, key;
 
        if (!keys) {
            me._keys = keys = [];
 
            for (= store.length; i--;) {
                key = store.key(i);
 
                if (key.length > n) {
                    if (prefix === key.substring(0, n)) {
                        keys.push(key.substring(n));
                    }
                }
            }
        }
 
        return keys;
    },
 
    /**
     * Call this method when finished with an instance returned by `get` instead of calling
     * `destroy`. When the last shared use of this instance calls `release`, the `destroy`
     * method is called automatically.
     * 
     * *NOTE:* Failing to call this method will result in memory leaks.
     */
    release: function() {
        if (! --this._users) {
            this.destroy();
        }
    },
 
    /**
     * @method
     * @static
     * @private
     */
    save: Ext.emptyFn,
 
    /**
     * Removes all of the keys of this storage.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     */
    clear: function() {
        var me = this,
            store = me._store,
            prefix = me.prefix,
            keys = me._keys || me.getKeys(),
            i;
 
        for (= keys.length; i--;) {
            store.removeItem(prefix + keys[i]);
        }
 
        keys.length = 0;
    },
 
    /**
     * Returns the specified key given its `index`. These keys have the scoping prefix
     * removed so they match what was passed to `setItem`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {Number} index The index of the desired key.
     * @return {String} The key.
     */
    key: function(index) {
        var keys = this._keys || this.getKeys();
 
        return (0 <= index && index < keys.length) ? keys[index] : null;
    },
 
    /**
     * Returns the value associated with the given `key`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {String} key The key.
     * @return {String} The value associated with the given `key`.
     */
    getItem: function(key) {
        var k = this.prefix + key;
 
        return this._store.getItem(k);
    },
 
    /**
     * Removes the value associated with the given `key`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {String} key The key.
     */
    removeItem: function(key) {
        var me = this,
            k = me.prefix + key,
            store = me._store,
            keys = me._keys,
            length = store.length;
 
        store.removeItem(k);
 
        if (keys && length !== store.length) {
            if (me.lazyKeys) {
                me._keys = null;
            }
            else {
                Ext.Array.remove(keys, key);
            }
        }
    },
 
    /**
     * Sets the value associated with the given `key`.
     * **NOTE:** This method conforms to the standard HTML5 Storage interface.
     * @param {String} key The key.
     * @param {String} value The new associated value for `key`.
     */
    setItem: function(key, value) {
        var me = this,
            k = me.prefix + key,
            store = me._store,
            length = store.length,
            keys = me._keys;
 
        store.setItem(k, value);
 
        if (keys && length !== store.length) {
            // Good news here - maintaining the keys collection is easy in this case.
            keys.push(key);
        }
    }
});