﻿/*
* jCacher - Client-side Cache Plugin for jQuery
* Version: 1.1.0 (2010-03-06)
*
* Author: Andreas Brantmo
* Website: http://plugins.jquery.com/project/ClientCache
*
* Dual licensed under:
* MIT: http://www.opensource.org/licenses/mit-license.php
* GPL Version 2: http://www.opensource.org/licenses/gpl-3.0.html
*/

(function($, undefined) {

    // Create the cache manager and attach it to the
    // global object, e.g jQuery.
    $.jCacher = new function() {

        // Save a reference to the current object
        var cache = this;

        // Reference to the current jQuery instance of 
        // the jCacher object.
        var $this = $(this);

        // Set current version
        cache.version = "1.1.0";

        // The number of items in the cache
        cache.count = 0;

        //var useLocalStorage = false;

        // Id for the current setTimeout.
        var currentTimeout;

        // The key of the next item to be removed from the
        // cache, based on last schedule.
        var nextKey;

        // Next scheduled check.
        var nextCheck;

        // Data storage object
        var store = new storage(false);

        // Adds the specified number of seconds
        // to a date object.
        var addMilliseconds = function(date, milliseconds) {

            return new Date(date.getTime() + milliseconds);

        };

        // Internal function for removing an item from cache.
        var removeItem = function(key, reason) {

            var itm = store.getCacheItem(key);

            if (key !== null && key !== undefined && itm !== null) {

                cache.count--;

                // Get dependency mappings for the cache item
                var mappings = store.getDependencyMappings(key);

                // Remove the cache item from storage
                store.removeCacheItem(key);

                // Trigger itemremoved event
                onitemremoved(itm, reason);

                removeDependencies(mappings);

            }

            return itm !== undefined;

        };

        var removeDependencies = function(mappings) {

            // Loop through the mappings and request them to be removed
            for (var i = 0; i < mappings.length; i++) {

                removeItem(mappings[i], "dependencyChanged");

            }

        };

        // Goes through all items in the cache and removes
        // them if expired.
        var validate = function() {

            var now = new Date();
            var items = store.getCacheItems();
            var rebuildSchedule = false;

            for (var i = 0; i < items.length; i++) {

                var item = items[i];

                if (item.expires <= now) {

                    rebuildSchedule = true;
                    removeItem(item.key, "expired");

                }
            }

            // Rebuild the schedule if items were removed
            if (rebuildSchedule) {
                schedule();
            }
        };

        // Calculates the next check
        var schedule = function(item) {

            // If no cacheitem is passed to the function,
            // calculate next check based on all
            // existing items in the cache.
            if (item === undefined) {

                nextCheck = null;
                nextKey = null;

                // Clear the current timeout
                if (currentTimeout) {

                    clearTimeout(currentTimeout);

                }

                var items = store.getCacheItems();

                // Calculate next expire based on existing cache items.
                for (var i = 0; i < items.length; i++) {
                    var itm = items[i];
                    if (nextCheck) {
                        if (itm.expires < nextCheck) {
                            nextCheck = itm.expires;
                            nextKey = itm.key;
                        }
                    }
                    else {
                        nextCheck = itm.expires;
                        nextKey = itm.key;
                    }
                }

                if (nextCheck) {

                    setTimer();

                }
                else {

                    currentTimeout = null;

                }
            }

            // If a cacheitem is passed to the function,
            // set the timer to its expire value if it's
            // earlier than nextCheck or if nextCheck is
            // undefined.
            else if (nextCheck == undefined || (nextCheck && item.expires < nextCheck)) {

                // Clear the current timeout
                if (currentTimeout) {

                    clearTimeout(currentTimeout);

                }

                nextCheck = item.expires;

                setTimer();

            }

        };

        var setTimer = function() {

            if (nextCheck) {

                var now = new Date();

                // Calculate time in milliseconds from now until next check
                var timeUntilNextCheck = nextCheck.getTime() - now.getTime() + 100;

                // Init a setTimeout if next check is in the future
                if (timeUntilNextCheck > 0) {

                    currentTimeout = setTimeout(validate, timeUntilNextCheck);

                }

                // Otherwise do the validation immediately
                else {

                    validate();

                }

            }

        }

        // Triggers itemremoved event
        var onitemremoved = function(item, reason) {

            $this.trigger("itemremoved", [item, reason]);

        };

        cache.itemremoved = function(fn) {

            $this.bind("itemremoved", fn);

        };

        // Adds a new item to the cache
        cache.add = function(key, value, slidingExpiration, absoluteExpiration, dependencies, onRemoved) {

            if (value !== undefined) {

                // Increase item count if key is not already in the cache
                if (store.getCacheKeys().indexOf(key) == -1) {

                    cache.count++;

                }
                // If the key exists, invalidate dependencies
                else {

                    // Get dependency mappings for the cache item
                    var mappings = store.getDependencyMappings(key);

                    removeDependencies(mappings);

                }

                // Calculate the expire date.
                var expires;
                if (slidingExpiration || absoluteExpiration) {

                    if (slidingExpiration) {

                        expires = addMilliseconds(new Date(), (slidingExpiration * 1000));

                    }

                    else if (absoluteExpiration) {

                        expires = absoluteExpiration;

                    }

                }

                // Register dependencies
                var remove = false;
                if (dependencies) {

                    // Returns false if a dependency could not be registered,
                    // if the target key was not found.
                    if (!store.registerDependencies(key, dependencies)) {

                        remove = true;

                    }

                }

                var item = new cacheItem(key, value, expires, slidingExpiration)

                // Adds the cache item to the cache.
                store.addCacheItem(item);

                // If the item is set to expire, rebuild the schedule, but
                // only if it's earlier than nextCheck.
                if (!remove) {
                    if (expires && (nextCheck === undefined || expires < nextCheck) || (nextKey == key || nextCheck === null)) {

                        schedule(item);

                    }
                }
                else {

                    removeItem(key, "dependencyChanged");

                }
            }

        };

        // Gets an item from the cache. If the key does not exist or if 
        // the item has expired, return null.
        cache.get = function(key) {

            // Get item from storage
            var itm = store.getCacheItem(key);

            if (itm) {

                // Current timestamp
                var now = new Date();

                // If the item has sliding expiration, change the expires property
                // and rebuild the schedule.
                if (itm.slidingExpiration) {

                    itm.expires = addMilliseconds(now, (itm.slidingExpiration * 1000));

                    // Only rebuild the schedule if it expires earlier than nextCheck
                    if ((key == nextKey) || (nextCheck && itm.expires < nextCheck)) {

                        schedule();

                    }
                    else {
                        var b = true;
                    }
                }

                // If the item has expired, return null
                if (itm.expires && itm.expires < now) {

                    return null;

                }

                return itm;

            }

            return null;

        };

        // Removes an item from the cache
        cache.remove = function(key) {

            if (key !== undefined && key !== null && key !== NaN && cache.count > 0) {

                return removeItem(key, "removed");

                // Rebuild the schedule if next check
                // is based on this cache item.
                if (nextKey == key) {

                    schedule();

                }
            }
        };

        // Removes all items from the cache
        cache.clear = function() {

            if (cache.count > 0) {

                cache.count = 0;

                store.clear();

                if (currentTimeout !== null) {

                    clearTimeout(currentTimeout);

                    currentTimeout = null;

                }

            }

        };

        // Build the schedule if items exist in the cache
        if (store.getCacheItems().length > 0) {

            schedule();

        }
    }

    // Represents a cache item.
    function cacheItem(key, value, expires, slidingExpiration) {

        this.key = key;

        this.value = value;

        this.expires = expires;

        this.slidingExpiration = slidingExpiration;
    }

    // Represents a dependency mapper
    function dependencyMapper(key, mappings) {

        this.key = key;

        this.mappings = mappings;
    }

    function storage(useLocalStorage) {

        // The cache items
        var _items = [];

        // The cache keys
        var _keys = [];

        // The cache dependency mappings
        var _dependencyMappings = [];

        (function() {

            if (useLocalStorage && window.localStorage) {

                // Create an empty object in localStorage if undefined
                if (!window.localStorage.jCacher) {
                    window.localStorage.jCacher = jQuery.toJSON({ items: [], dependencyMappings: [] });
                }

                // Else get the cache object from local storage
                else {

                    var cacheItem = jQuery.parseJSON(window.localStorage.jCacher);

                    // Loop all items and make the expires property to a Date
                    for (var i = 0; i < cacheItem.items.length; i++) {
                        var item = cacheItem.items[i];
                        item.expires = new Date(item.expires);
                        _items.push(item);
                    }
                    _dependencyMappings = cacheItem.dependencyMappings;

                }

                for (var i = 0; i < _items.length; i++) {

                    _keys.push(_items[i].key);

                }
            }

        })();

        // Gets a cache item by key
        this.getCacheItem = function(key) {

            var index = _keys.indexOf(key);
            return index > -1 ? _items[index] : null;

        };

        // Gets all cache items
        this.getCacheItems = function() {

            return _items;

        };

        // Removes a cache item from storage
        this.removeCacheItem = function(key) {

            var indexToRemove = _keys.indexOf(key);

            if (useLocalStorage && window.localStorage) {

                // Get cache object from localStorage
                var cacheItem = jQuery.parseJSON(window.localStorage.jCacher);

                // Remove from local storage object
                cacheItem.dependencyMappings.splice(indexToRemove, 1);
                cacheItem.items.splice(indexToRemove, 1);

                // Put the JSONized object to localStorage
                window.localStorage.jCacher = jQuery.toJSON(cacheItem);

            }

            // Remove from local objects
            _items.splice(indexToRemove, 1);
            _keys.splice(indexToRemove, 1);
            _dependencyMappings.splice(indexToRemove, 1);

        };

        // Adds a cache item to storage
        this.addCacheItem = function(value) {

            var index = _keys.indexOf(value.key);

            if (index == -1) {

                var mapper = new dependencyMapper(value.key, []);

                _items.push(value);
                _keys.push(value.key);
                _dependencyMappings.push(mapper);

                if (useLocalStorage && window.localStorage) {

                    var cacheItem = jQuery.parseJSON(window.localStorage.jCacher);

                    var jsonValue = (function() {

                        var obj = new Object();
                        obj.expires = value.expires.getTime();
                        obj.key = value.key;
                        obj.value = value.value;
                        obj.slidingExpiration = value.slidingExpiration;
                        return obj;

                    })();

                    cacheItem.items.push(jsonValue);
                    cacheItem.dependencyMappings.push(mapper);
                    window.localStorage.jCacher = jQuery.toJSON(cacheItem);
                }

            }
            else {

                _items[index] = value;

                if (useLocalStorage && window.localStorage) {

                    var cacheItem = jQuery.parseJSON(window.localStorage.jCacher);

                    cacheItem.items[index] = value;

                    window.localStorage.jCacher = jQuery.toJSON(cacheItem);
                }

            }

        };

        // Gets all cache keys
        this.getCacheKeys = function() {

            return _keys;

        };

        // Register dependencies to storage
        this.registerDependencies = function(key, dependencies) {

            for (var i = 0; i < dependencies.length; i++) {

                var mappingsIndex = _keys.indexOf(dependencies[i]);

                if (mappingsIndex != -1) {

                    if (_dependencyMappings[mappingsIndex].mappings.indexOf(key) == -1) {

                        _dependencyMappings[mappingsIndex].mappings.push(key);

                        if (useLocalStorage && window.localStorage) {

                            var cacheItem = jQuery.parseJSON(window.localStorage.jCacher);

                            cacheItem.dependencyMappings[mappingsIndex].mappings.push(key);

                            window.localStorage.jCacher = jQuery.toJSON(cacheItem);

                        }

                    }
                }
                else {

                    return false;

                }
            }

            return true;

        };

        // Gets dependency mappings for the specified cache key
        this.getDependencyMappings = function(key) {

            var index = _keys.indexOf(key);
            return index > -1 ? _dependencyMappings[index].mappings : null;

        };

        // Clears all items in the storage.
        this.clear = function() {

            if (window.localStorage) {

                window.localStorage.removeItem("jCacher");

            }

            _items = [];
            _dependencyMappings = [];
            _keys = [];

        };

    };

    if (!Array.indexOf) {
        Array.prototype.indexOf = function(obj) {
            for (var i = 0; i < this.length; i++) {
                if (this[i] == obj) {
                    return i;
                }
            }
            return -1;
        }
    }

})(jQuery);
