/* jshint browser: true */
/* global define: false */

// AMD shim
(function(root, factory) {

    "use strict";

    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {
        root.simpleStorage = factory();
    }

}(this, function() {

    "use strict";

    var
        VERSION = "0.1.2",

        /* This is the object, that holds the cached values */
        _storage = false,

        /* How much space does the storage take */
        _storage_size = 0,

        _storage_available = false,

        _ttl_timeout = null;

    // This method might throw as it touches localStorage and doing so
    // can be prohibited in some environments
    function _init(){

        // If localStorage does not exist, the following throws
        // This is intentional
        window.localStorage.setItem("__simpleStorageInitTest", "tmpval");
        window.localStorage.removeItem("__simpleStorageInitTest");

        // Load data from storage
        _load_storage();

        // remove dead keys
        _handleTTL();

        // start listening for changes
        _setupUpdateObserver();

        // handle cached navigation
        if("addEventListener" in window){
            window.addEventListener("pageshow", function(event){
                if(event.persisted){
                    _reloadData();
                }
            }, false);
        }

        _storage_available = true;
    }

    /**
     * Sets up a storage change observer
     */
    function _setupUpdateObserver(){
        if("addEventListener" in window){
            window.addEventListener("storage", _reloadData, false);
        }else{
            document.attachEvent("onstorage", _reloadData);
        }
    }

    /**
     * Reload data from storage when needed
     */
    function _reloadData(){
        try{
            _load_storage();
        }catch(E){
            _storage_available = false;
            return;
        }
        _handleTTL();
    }

    function _load_storage(){
        var source = localStorage.getItem("simpleStorage");

        try{
            _storage = JSON.parse(source) || {};
        }catch(E){
            _storage = {};
        }

        _storage_size = _get_storage_size();
    }

    function _save(){
        try{
            localStorage.setItem("simpleStorage", JSON.stringify(_storage));
            _storage_size = _get_storage_size();
        }catch(E){
            return E;
        }
        return true;
    }

    function _get_storage_size() {
        var source = localStorage.getItem("simpleStorage");
        return source ? String(source).length : 0;
    }

    function _handleTTL(){
        var curtime, i, len, expire, keys, nextExpire = Infinity, expiredKeysCount = 0;

        clearTimeout(_ttl_timeout);

        if(!_storage || !_storage.__simpleStorage_meta || !_storage.__simpleStorage_meta.TTL){
            return;
        }

        curtime = +new Date();
        keys = _storage.__simpleStorage_meta.TTL.keys || [];
        expire = _storage.__simpleStorage_meta.TTL.expire || {};

        for(i = 0, len = keys.length; i<len; i++){
            if(expire[keys[i]] <= curtime){
                expiredKeysCount++;
                delete _storage[keys[i]];
                delete expire[keys[i]];
            }else{
                if(expire[keys[i]] < nextExpire){
                    nextExpire = expire[keys[i]];
                }
                break;
            }
        }

        // set next check
        if(nextExpire != Infinity){
            _ttl_timeout = setTimeout(_handleTTL, Math.min(nextExpire - curtime, 0x7FFFFFFF));
        }

        // remove expired from TTL list and save changes
        if(expiredKeysCount){
            keys.splice(0, expiredKeysCount);

            _cleanMetaObject();
            _save();
        }
    }

    function _setTTL(key, ttl){
        var curtime = +new Date(), i, len, added = false;

        ttl = Number(ttl) || 0;

        // Set TTL value for the key
        if(ttl !== 0){
            // If key exists, set TTL
            if(_storage.hasOwnProperty(key)){

                if(!_storage.__simpleStorage_meta){
                    _storage.__simpleStorage_meta = {};
                }

                if(!_storage.__simpleStorage_meta.TTL){
                    _storage.__simpleStorage_meta.TTL = {expire: {}, keys:[]};
                }

                _storage.__simpleStorage_meta.TTL.expire[key] = curtime + ttl;

                // remove from keys array
                if(_storage.__simpleStorage_meta.TTL.expire.hasOwnProperty(key)){
                    for(i = 0, len = _storage.__simpleStorage_meta.TTL.keys.length; i<len; i++){
                        if(_storage.__simpleStorage_meta.TTL.keys[i] == key){
                            _storage.__simpleStorage_meta.TTL.keys.splice(i);
                        }
                    }
                }

                // add to keys array, sorted by ttl
                for(i = 0, len = _storage.__simpleStorage_meta.TTL.keys.length; i<len; i++){
                    if(_storage.__simpleStorage_meta.TTL.expire[_storage.__simpleStorage_meta.TTL.keys[i]] > curtime + ttl){
                        _storage.__simpleStorage_meta.TTL.keys.splice(i, 0, key);
                    }
                }

                if(!added){
                    _storage.__simpleStorage_meta.TTL.keys.push(key);
                }
            }else{
                return false;
            }
        }else{
            // Remove TTL if set
            if(_storage && _storage.__simpleStorage_meta && _storage.__simpleStorage_meta.TTL){

                if(_storage.__simpleStorage_meta.TTL.expire.hasOwnProperty(key)){
                    delete _storage.__simpleStorage_meta.TTL.expire[key];
                    for(i = 0, len = _storage.__simpleStorage_meta.TTL.keys.length; i<len; i++){
                        if(_storage.__simpleStorage_meta.TTL.keys[i] == key){
                            _storage.__simpleStorage_meta.TTL.keys.splice(i, 1);
                            break;
                        }
                    }
                }

                _cleanMetaObject();
            }
        }

        // schedule next TTL check
        clearTimeout(_ttl_timeout);
        if(_storage && _storage.__simpleStorage_meta && _storage.__simpleStorage_meta.TTL && _storage.__simpleStorage_meta.TTL.keys.length){
            _ttl_timeout = setTimeout(_handleTTL, Math.min(Math.max(_storage.__simpleStorage_meta.TTL.expire[_storage.__simpleStorage_meta.TTL.keys[0]] - curtime, 0), 0x7FFFFFFF));
        }

        return true;
    }

    function _cleanMetaObject(){
        var updated = false, hasProperties = false, i;

        if(!_storage || !_storage.__simpleStorage_meta){
            return updated;
        }

        // If nothing to TTL, remove the object
        if(_storage.__simpleStorage_meta.TTL && !_storage.__simpleStorage_meta.TTL.keys.length){
            delete _storage.__simpleStorage_meta.TTL;
            updated = true;
        }

        // If meta object is empty, remove it
        for(i in _storage.__simpleStorage_meta){
            if(_storage.__simpleStorage_meta.hasOwnProperty(i)){
                hasProperties = true;
                break;
            }
        }

        if(!hasProperties){
            delete _storage.__simpleStorage_meta;
            updated = true;
        }

        return updated;
    }

    ////////////////////////// PUBLIC INTERFACE /////////////////////////

    try{
        _init();
    }catch(E){}

    return {

        version: VERSION,

        canUse: function(){
            return !!_storage_available;
        },

        set: function(key, value, options){
            if(key == "__simpleStorage_meta"){
                return false;
            }

            if(!_storage){
                return false;
            }

            // undefined values are deleted automatically
            if(typeof value == "undefined"){
                return this.deleteKey(key);
            }

            options = options || {};

            // Check if the value is JSON compatible (and remove reference to existing objects/arrays)
            try{
                value = JSON.parse(JSON.stringify(value));
            }catch(E){
                return E;
            }

            _storage[key] = value;

            _setTTL(key, options.TTL || 0);

            return _save();
        },

        get: function(key){
            if(!_storage){
                return false;
            }

            if(_storage.hasOwnProperty(key) && key != "__simpleStorage_meta"){
                // TTL value for an existing key is either a positive number or an Infinity
                if(this.getTTL(key)){
                    return _storage[key];
                }
            }
        },

        deleteKey: function(key){

            if(!_storage){
                return false;
            }

            if(key in _storage){
                delete _storage[key];

                _setTTL(key, 0);

                return _save();
            }

            return false;
        },

        setTTL: function(key, ttl){
            if(!_storage){
                return false;
            }

            _setTTL(key, ttl);

            return _save();
        },

        getTTL: function(key){
            var ttl;

            if(!_storage){
                return false;
            }

            if(_storage.hasOwnProperty(key)){
                if(_storage.__simpleStorage_meta &&
                    _storage.__simpleStorage_meta.TTL &&
                    _storage.__simpleStorage_meta.TTL.expire &&
                    _storage.__simpleStorage_meta.TTL.expire.hasOwnProperty(key)){

                    ttl = Math.max(_storage.__simpleStorage_meta.TTL.expire[key] - (+new Date()) || 0, 0);

                    return ttl || false;
                }else{
                    return Infinity;
                }
            }

            return false;
        },

        flush: function(){
            if(!_storage){
                return false;
            }

            _storage = {};
            try{
                localStorage.removeItem("simpleStorage");
                return true;
            }catch(E){
                return E;
            }
        },

        index: function(){
            if(!_storage){
                return false;
            }

            var index = [], i;
            for(i in _storage){
                if(_storage.hasOwnProperty(i) && i != "__simpleStorage_meta"){
                    index.push(i);
                }
            }
            return index;
        },

        storageSize: function(){
            return _storage_size;
        }
    };

}));
