/**
 * @private
 * @class Ext.util.LruCache
 * @extend Ext.util.HashMap
 * A linked {@link Ext.util.HashMap HashMap} implementation which maintains most recently accessed
 * items at the end of the list, and purges the cache down to the most recently accessed
 * {@link #maxSize} items upon add.
 */
Ext.define('Ext.util.LruCache', {
    extend: 'Ext.util.HashMap',
 
    config: {
        /** 
         * @cfg {Number} maxSize
         * The maximum size the cache is allowed to grow to before further additions
         * cause removal of the least recently used entry.
         */
        maxSize: null
    },
 
    /**
     * @method add
     * @inheritdoc
     */
    add: function(key, newValue) {
        var me = this,
            entry, last;
 
        me.removeAtKey(key);
        last = me.last;
        entry = {
            prev: last,
            next: null,
            key: key,
            value: newValue
        };
 
        if (last) {
            // If the list is not empty, update the last entry
            last.next = entry;
        }
        else {
            // List is empty
            me.first = entry;
        }
 
        me.last = entry;
        me.callParent([key, entry]);
        me.prune();
 
        return newValue;
    },
 
    /**
     * @private
     */
    insertBefore: function(key, newValue, sibling) {
        var me = this,
            existingKey, entry;
 
        // NOT an assignment.
        // If there is a following sibling
        // eslint-disable-next-line no-cond-assign
        if (sibling = this.map[this.findKey(sibling)]) {
            existingKey = me.findKey(newValue);
 
            // "new" value is in the list.
            if (existingKey) {
                me.unlinkEntry(entry = me.map[existingKey]);
            }
            // Genuinely new: create an entry for it.
            else {
                entry = {
                    prev: sibling.prev,
                    next: sibling,
                    key: key,
                    value: newValue
                };
            }
 
            if (sibling.prev) {
                entry.prev.next = entry;
            }
            else {
                me.first = entry;
            }
 
            entry.next = sibling;
            sibling.prev = entry;
            me.prune();
 
            return newValue;
        }
        // No following sibling, it's just an add.
        else {
            return me.add(key, newValue);
        }
    },
 
    /**
     * @method get
     * @inheritdoc
     */
    get: function(key) {
        var entry = this.map[key];
 
        if (entry) {
 
            // If it's not the end, move to end of list on get
            if (entry.next) {
                this.moveToEnd(entry);
            }
 
            return entry.value;
        }
    },
 
    /**
     * @private
     */
    removeAtKey: function(key) {
        this.unlinkEntry(this.map[key]);
 
        return this.callParent(arguments);
    },
 
    /**
     * @method clear
     * @inheritdoc
     * @param initial (private)
     */
    clear: function(initial) {
        this.first = this.last = null;
 
        return this.callParent([initial]);
    },
 
    /**
     * @private
     */
    unlinkEntry: function(entry) {
        // Stitch the list back up.
        if (entry) {
            if (entry.next) {
                entry.next.prev = entry.prev;
            }
            else {
                this.last = entry.prev;
            }
 
            if (entry.prev) {
                entry.prev.next = entry.next;
            }
            else {
                this.first = entry.next;
            }
 
            entry.prev = entry.next = null;
        }
    },
 
    /**
     * @private
     */
    moveToEnd: function(entry) {
        this.unlinkEntry(entry);
 
        // NOT an assignment.
        // If the list is not empty, update the last entry
        // eslint-disable-next-line no-cond-assign
        if (entry.prev = this.last) {
            this.last.next = entry;
        }
        // List is empty
        else {
            this.first = entry;
        }
 
        this.last = entry;
    },
 
    /**
     * @private
     */
    getArray: function(isKey) {
        var arr = [],
            entry = this.first;
 
        while (entry) {
            arr.push(isKey ? entry.key : entry.value);
            entry = entry.next;
        }
 
        return arr;
    },
 
    /**
     * Executes the specified function once for each item in the cache.
     * Returning false from the function will cease iteration.
     *
     * By default, iteration is from least recently used to most recent.
     *
     * The paramaters passed to the function are:
     * <div class="mdetail-params"><ul>
     * <li><b>key</b> : String<p class="sub-desc">The key of the item</p></li>
     * <li><b>value</b> : Number<p class="sub-desc">The value of the item</p></li>
     * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the hash</p></li>
     * </ul></div>
     * @param {Function} fn The function to execute.
     * @param {Object} scope The scope (<code>this</code> reference) to execute in. Defaults
     * to this LruCache.
     * @param {Boolean} [reverse=false] Pass <code>true</code> to iterate the list in reverse
     * (most recent first) order.
     * @return {Ext.util.LruCache} this
     */
    each: function(fn, scope, reverse) {
        var me = this,
            entry = reverse ? me.last : me.first,
            length = me.length;
 
        scope = scope || me;
 
        while (entry) {
            if (fn.call(scope, entry.key, entry.value, length) === false) {
                break;
            }
 
            entry = reverse ? entry.prev : entry.next;
        }
 
        return me;
    },
 
    /**
     * @private
     */
    findKey: function(value) {
        var key,
            map = this.map;
 
        for (key in map) {
            // Attention. Differs from subclass in that this compares the value property
            // of the entry.
            if (map.hasOwnProperty(key) && map[key].value === value) {
                return key;
            }
        }
 
        return undefined;
    },
 
    /**
     * Performs a shallow copy on this haLruCachesh.
     * @return {Ext.util.HashMap} The new hash object.
     */
    clone: function() {
        var newCache = new this.self(this.initialConfig),
            map = this.map,
            key;
 
        newCache.suspendEvents();
 
        for (key in map) {
            if (map.hasOwnProperty(key)) {
                newCache.add(key, map[key].value);
            }
        }
 
        newCache.resumeEvents();
 
        return newCache;
    },
 
    /**
     * Purge the least recently used entries if the maxSize has been exceeded.
     */
    prune: function() {
        var me = this,
            max = me.getMaxSize(),
            purgeCount = max ? (me.length - max) : 0;
 
        if (purgeCount > 0) {
            for (; me.first && purgeCount; purgeCount--) {
                me.removeAtKey(me.first.key);
            }
        }
    }
 
    /**
     * @method containsKey
     * @private
     */
 
    /**
     * @method contains
     * @private
     */
 
    /**
     * @method getKeys
     * @private
     */
 
    /**
     * @method getValues
     * @private
     */
});