/*
 * JS Storage Plugin
 *
 * Copyright (c) 2019 Julien Maurel
 *
 * Licensed under the MIT license:
 * http://www.opensource.org/licenses/mit-license.php
 *
 * Project home:
 * https://github.com/julien-maurel/js-storage
 *
 * Version: 1.1.0
 */
(function (factory) {
    var registeredInModuleLoader = false;
    if (typeof define === 'function' && define.amd) {
        define(factory);
        registeredInModuleLoader = true;
    }
    if (typeof exports === 'object') {
        module.exports = factory();
        registeredInModuleLoader = true;
    }
    if (!registeredInModuleLoader) {
        var OldStorages = window.Storages;
        var api = window.Storages = factory();
        api.noConflict = function () {
            window.Storages = OldStorages;
            return api;
        };
    }
}(function () {
    // Variables used by utilities functions (like isPlainObject...)
    var class2type = {};
    var toString = class2type.toString;
    var hasOwn = class2type.hasOwnProperty;
    var fnToString = hasOwn.toString;
    var ObjectFunctionString = fnToString.call(Object);
    var getProto = Object.getPrototypeOf;
    var apis = {};

    // Prefix to use with cookie fallback
    var cookie_local_prefix = "ls_";
    var cookie_session_prefix = "ss_";

    // Get items from a storage
    function _get() {
        var storage = this._type, l = arguments.length, s = window[storage], a = arguments, a0 = a[0], vi, ret, tmp, i, j;
        if (l < 1) {
            throw new Error('Minimum 1 argument must be given');
        } else if (Array.isArray(a0)) {
            // If second argument is an array, return an object with value of storage for each item in this array
            ret = {};
            for (i in a0) {
                if (a0.hasOwnProperty(i)) {
                    vi = a0[i];
                    try {
                        ret[vi] = JSON.parse(s.getItem(vi));
                    } catch (e) {
                        ret[vi] = s.getItem(vi);
                    }
                }
            }
            return ret;
        } else if (l == 1) {
            // If only 1 argument, return value directly
            try {
                return JSON.parse(s.getItem(a0));
            } catch (e) {
                return s.getItem(a0);
            }
        } else {
            // If more than 1 argument, parse storage to retrieve final value to return it
            // Get first level
            try {
                ret = JSON.parse(s.getItem(a0));
                if (!ret) {
                    throw new ReferenceError(a0 + ' is not defined in this storage');
                }
            } catch (e) {
                throw new ReferenceError(a0 + ' is not defined in this storage');
            }
            // Parse next levels
            for (i = 1; i < l - 1; i++) {
                ret = ret[a[i]];
                if (ret === undefined) {
                    throw new ReferenceError([].slice.call(a, 0, i + 1).join('.') + ' is not defined in this storage');
                }
            }
            // If last argument is an array, return an object with value for each item in this array
            // Else return value normally
            if (Array.isArray(a[i])) {
                tmp = ret;
                ret = {};
                for (j in a[i]) {
                    if (a[i].hasOwnProperty(j)) {
                        ret[a[i][j]] = tmp[a[i][j]];
                    }
                }
                return ret;
            } else {
                return ret[a[i]];
            }
        }
    }

    // Set items of a storage
    function _set() {
        var storage = this._type, l = arguments.length, s = window[storage], a = arguments, a0 = a[0], a1 = a[1], vi, to_store = isNaN(a1) ? {} : [], type, tmp, i;
        if (l < 1 || !_isPlainObject(a0) && l < 2) {
            throw new Error('Minimum 2 arguments must be given or first parameter must be an object');
        } else if (_isPlainObject(a0)) {
            // If first argument is an object, set values of storage for each property of this object
            for (i in a0) {
                if (a0.hasOwnProperty(i)) {
                    vi = a0[i];
                    if (!_isPlainObject(vi) && !this.alwaysUseJson) {
                        s.setItem(i, vi);
                    } else {
                        s.setItem(i, JSON.stringify(vi));
                    }
                }
            }
            return a0;
        } else if (l == 2) {
            // If only 2 arguments, set value of storage directly
            if (typeof a1 === 'object' || this.alwaysUseJson) {
                s.setItem(a0, JSON.stringify(a1));
            } else {
                s.setItem(a0, a1);
            }
            return a1;
        } else {
            // If more than 3 arguments, parse storage to retrieve final node and set value
            // Get first level
            try {
                tmp = s.getItem(a0);
                if (tmp != null) {
                    to_store = JSON.parse(tmp);
                }
            } catch (e) {
            }
            tmp = to_store;
            // Parse next levels and set value
            for (i = 1; i < l - 2; i++) {
                vi = a[i];
                type = isNaN(a[i + 1]) ? "object" : "array";
                if (!tmp[vi] || type == "object" && !_isPlainObject(tmp[vi]) || type == "array" && !Array.isArray(tmp[vi])) {
                    if (type == "array") tmp[vi] = [];
                    else tmp[vi] = {};
                }
                tmp = tmp[vi];
            }
            tmp[a[i]] = a[i + 1];
            s.setItem(a0, JSON.stringify(to_store));
            return to_store;
        }
    }

    // Remove items from a storage
    function _remove() {
        var storage = this._type, l = arguments.length, s = window[storage], a = arguments, a0 = a[0], to_store, tmp, i, j;
        if (l < 1) {
            throw new Error('Minimum 1 argument must be given');
        } else if (Array.isArray(a0)) {
            // If first argument is an array, remove values from storage for each item of this array
            for (i in a0) {
                if (a0.hasOwnProperty(i)) {
                    s.removeItem(a0[i]);
                }
            }
            return true;
        } else if (l == 1) {
            // If only 2 arguments, remove value from storage directly
            s.removeItem(a0);
            return true;
        } else {
            // If more than 2 arguments, parse storage to retrieve final node and remove value
            // Get first level
            try {
                to_store = tmp = JSON.parse(s.getItem(a0));
            } catch (e) {
                throw new ReferenceError(a0 + ' is not defined in this storage');
            }
            // Parse next levels and remove value
            for (i = 1; i < l - 1; i++) {
                tmp = tmp[a[i]];
                if (tmp === undefined) {
                    throw new ReferenceError([].slice.call(a, 1, i).join('.') + ' is not defined in this storage');
                }
            }
            // If last argument is an array,remove value for each item in this array
            // Else remove value normally
            if (Array.isArray(a[i])) {
                for (j in a[i]) {
                    if (a[i].hasOwnProperty(j)) {
                        delete tmp[a[i][j]];
                    }
                }
            } else {
                delete tmp[a[i]];
            }
            s.setItem(a0, JSON.stringify(to_store));
            return true;
        }
    }

    // Remove all items from a storage
    function _removeAll(reinit_ns) {
        var keys = _keys.call(this), i;
        for (i in keys) {
            if (keys.hasOwnProperty(i)) {
                _remove.call(this, keys[i]);
            }
        }
        // Reinitialize all namespace storages
        if (reinit_ns) {
            for (i in apis.namespaceStorages) {
                if (apis.namespaceStorages.hasOwnProperty(i)) {
                    _createNamespace(i);
                }
            }
        }
    }

    // Check if items of a storage are empty
    function _isEmpty() {
        var l = arguments.length, a = arguments, a0 = a[0], i;
        if (l == 0) {
            // If no argument, test if storage is empty
            return (_keys.call(this).length == 0);
        } else if (Array.isArray(a0)) {
            // If first argument is an array, test each item of this array and return true only if all items are empty
            for (i = 0; i < a0.length; i++) {
                if (!_isEmpty.call(this, a0[i])) {
                    return false;
                }
            }
            return true;
        } else {
            // If at least 1 argument, try to get value and test it
            try {
                var v = _get.apply(this, arguments);
                // Convert result to an object (if last argument is an array, _get return already an object) and test each item
                if (!Array.isArray(a[l - 1])) {
                    v = {'totest': v};
                }
                for (i in v) {
                    if (v.hasOwnProperty(i) && !(
                            (_isPlainObject(v[i]) && _isEmptyObject(v[i])) ||
                            (Array.isArray(v[i]) && !v[i].length) ||
                            (typeof v[i] !== 'boolean' && !v[i])
                        )) {
                        return false;
                    }
                }
                return true;
            } catch (e) {
                return true;
            }
        }
    }

    // Check if items of a storage exist
    function _isSet() {
        var l = arguments.length, a = arguments, a0 = a[0], i;
        if (l < 1) {
            throw new Error('Minimum 1 argument must be given');
        }
        if (Array.isArray(a0)) {
            // If first argument is an array, test each item of this array and return true only if all items exist
            for (i = 0; i < a0.length; i++) {
                if (!_isSet.call(this, a0[i])) {
                    return false;
                }
            }
            return true;
        } else {
            // For other case, try to get value and test it
            try {
                var v = _get.apply(this, arguments);
                // Convert result to an object (if last argument is an array, _get return already an object) and test each item
                if (!Array.isArray(a[l - 1])) {
                    v = {'totest': v};
                }
                for (i in v) {
                    if (v.hasOwnProperty(i) && !(v[i] !== undefined && v[i] !== null)) {
                        return false;
                    }
                }
                return true;
            } catch (e) {
                return false;
            }
        }
    }

    // Get keys of a storage or of an item of the storage
    function _keys() {
        var storage = this._type, l = arguments.length, s = window[storage], keys = [], o = {};
        // If at least 1 argument, get value from storage to retrieve keys
        // Else, use storage to retrieve keys
        if (l > 0) {
            o = _get.apply(this, arguments);
        } else {
            o = s;
        }
        if (o && o._cookie) {
            // If storage is a cookie, use js-cookie to retrieve keys
            var cookies = Cookies.get();
            for (var key in cookies) {
                if (cookies.hasOwnProperty(key) && key != '') {
                    keys.push(key.replace(o._prefix, ''));
                }
            }
        } else {
            for (var i in o) {
                if (o.hasOwnProperty(i)) {
                    keys.push(i);
                }
            }
        }
        return keys;
    }

    // Create new namespace storage
    function _createNamespace(name) {
        if (!name || typeof name != "string") {
            throw new Error('First parameter must be a string');
        }
        if (storage_available) {
            if (!window.localStorage.getItem(name)) {
                window.localStorage.setItem(name, '{}');
            }
            if (!window.sessionStorage.getItem(name)) {
                window.sessionStorage.setItem(name, '{}');
            }
        } else {
            if (!window.localCookieStorage.getItem(name)) {
                window.localCookieStorage.setItem(name, '{}');
            }
            if (!window.sessionCookieStorage.getItem(name)) {
                window.sessionCookieStorage.setItem(name, '{}');
            }
        }
        var ns = {
            localStorage: _extend({}, apis.localStorage, {_ns: name}),
            sessionStorage: _extend({}, apis.sessionStorage, {_ns: name})
        };
        if (cookies_available) {
            if (!window.cookieStorage.getItem(name)) {
                window.cookieStorage.setItem(name, '{}');
            }
            ns.cookieStorage = _extend({}, apis.cookieStorage, {_ns: name});
        }
        apis.namespaceStorages[name] = ns;
        return ns;
    }

    // Test if storage is natively available on browser
    function _testStorage(name) {
        var foo = 'jsapi';
        try {
            if (!window[name]) {
                return false;
            }
            window[name].setItem(foo, foo);
            window[name].removeItem(foo);
            return true;
        } catch (e) {
            return false;
        }
    }

    // Test if a variable is a plain object (from jQuery)
    function _isPlainObject(obj) {
        var proto, Ctor;

        // Detect obvious negatives
        // Use toString instead of jQuery.type to catch host objects
        if (!obj || toString.call(obj) !== "[object Object]") {
            return false;
        }

        proto = getProto(obj);

        // Objects with no prototype (e.g., `Object.create( null )`) are plain
        if (!proto) {
            return true;
        }

        // Objects with prototype are plain iff they were constructed by a global Object function
        Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
        return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
    }

    // Test if a variable is an empty object (from jQuery)
    function _isEmptyObject(obj) {
        var name;

        for (name in obj) {
            return false;
        }
        return true;
    }

    // Merge objects
    function _extend() {
        var i = 1;
        var result = arguments[0];
        for (; i < arguments.length; i++) {
            var attributes = arguments[i];
            for (var key in attributes) {
                if (attributes.hasOwnProperty(key)) {
                    result[key] = attributes[key];
                }
            }
        }
        return result;
    }

    // Check if storages are natively available on browser and check is js-cookie is present
    var storage_available = _testStorage('localStorage');
    var cookies_available = typeof Cookies !== 'undefined';

    // Namespace object
    var storage = {
        _type: '',
        _ns: '',
        _callMethod: function (f, a) {
            a = Array.prototype.slice.call(a);
            var p = [], a0 = a[0];
            if (this._ns) {
                p.push(this._ns);
            }
            if (typeof a0 === 'string' && a0.indexOf('.') !== -1) {
                a.shift();
                [].unshift.apply(a, a0.split('.'));
            }
            [].push.apply(p, a);
            return f.apply(this, p);
        },
        // Define if plugin always use JSON to store values (even to store simple values like string, int...) or not
        alwaysUseJson: false,
        // Get items. If no parameters and storage have a namespace, return all namespace
        get: function () {
            if (!storage_available && !cookies_available){
                return null;
            }
            return this._callMethod(_get, arguments);
        },
        // Set items
        set: function () {
            var l = arguments.length, a = arguments, a0 = a[0];
            if (l < 1 || !_isPlainObject(a0) && l < 2) {
                throw new Error('Minimum 2 arguments must be given or first parameter must be an object');
            }
            if (!storage_available && !cookies_available){
                return null;
            }
            // If first argument is an object and storage is a namespace storage, set values individually
            if (_isPlainObject(a0) && this._ns) {
                for (var i in a0) {
                    if (a0.hasOwnProperty(i)) {
                        this._callMethod(_set, [i, a0[i]]);
                    }
                }
                return a0;
            } else {
                var r = this._callMethod(_set, a);
                if (this._ns) {
                    return r[a0.split('.')[0]];
                } else {
                    return r;
                }
            }
        },
        // Delete items
        remove: function () {
            if (arguments.length < 1) {
                throw new Error('Minimum 1 argument must be given');
            }
            if (!storage_available && !cookies_available){
                return null;
            }
            return this._callMethod(_remove, arguments);
        },
        // Delete all items
        removeAll: function (reinit_ns) {
            if (!storage_available && !cookies_available){
                return null;
            }
            if (this._ns) {
                this._callMethod(_set, [{}]);
                return true;
            } else {
                return this._callMethod(_removeAll, [reinit_ns]);
            }
        },
        // Items empty
        isEmpty: function () {
            if (!storage_available && !cookies_available){
                return null;
            }
            return this._callMethod(_isEmpty, arguments);
        },
        // Items exists
        isSet: function () {
            if (arguments.length < 1) {
                throw new Error('Minimum 1 argument must be given');
            }
            if (!storage_available && !cookies_available){
                return null;
            }
            return this._callMethod(_isSet, arguments);
        },
        // Get keys of items
        keys: function () {
            if (!storage_available && !cookies_available){
                return null;
            }
            return this._callMethod(_keys, arguments);
        }
    };

    // Use js-cookie for compatibility with old browsers and give access to cookieStorage
    if (cookies_available) {
        // sessionStorage is valid for one window/tab. To simulate that with cookie, we set a name for the window and use it for the name of the cookie
        if (!window.name) {
            window.name = Math.floor(Math.random() * 100000000);
        }
        var cookie_storage = {
            _cookie: true,
            _prefix: '',
            _expires: null,
            _path: null,
            _domain: null,
            _secure: false,
            setItem: function (n, v) {
                Cookies.set(this._prefix + n, v, {expires: this._expires, path: this._path, domain: this._domain, secure: this._secure});
            },
            getItem: function (n) {
                return Cookies.get(this._prefix + n);
            },
            removeItem: function (n) {
                return Cookies.remove(this._prefix + n, {path: this._path});
            },
            clear: function () {
                var cookies = Cookies.get();
                for (var key in cookies) {
                    if (cookies.hasOwnProperty(key) && key != '') {
                        if (!this._prefix && key.indexOf(cookie_local_prefix) === -1 && key.indexOf(cookie_session_prefix) === -1 || this._prefix && key.indexOf(this._prefix) === 0) {
                            Cookies.remove(key);
                        }
                    }
                }
            },
            setExpires: function (e) {
                this._expires = e;
                return this;
            },
            setPath: function (p) {
                this._path = p;
                return this;
            },
            setDomain: function (d) {
                this._domain = d;
                return this;
            },
            setSecure: function (s) {
                this._secure = s;
                return this;
            },
            setConf: function (c) {
                if (c.path) {
                    this._path = c.path;
                }
                if (c.domain) {
                    this._domain = c.domain;
                }
                if (c.secure) {
                    this._secure = c.secure;
                }
                if (c.expires) {
                    this._expires = c.expires;
                }
                return this;
            },
            setDefaultConf: function () {
                this._path = this._domain = this._expires = null;
                this._secure = false;
            }
        };
        if (!storage_available) {
            window.localCookieStorage = _extend({}, cookie_storage, {
                _prefix: cookie_local_prefix,
                _expires: 365 * 10,
                _secure: true
            });
            window.sessionCookieStorage = _extend({}, cookie_storage, {
                _prefix: cookie_session_prefix + window.name + '_',
                _secure: true
            });
        }
        window.cookieStorage = _extend({}, cookie_storage);
        // cookieStorage API
        apis.cookieStorage = _extend({}, storage, {
            _type: 'cookieStorage',
            setExpires: function (e) {
                window.cookieStorage.setExpires(e);
                return this;
            },
            setPath: function (p) {
                window.cookieStorage.setPath(p);
                return this;
            },
            setDomain: function (d) {
                window.cookieStorage.setDomain(d);
                return this;
            },
            setSecure: function (s) {
                window.cookieStorage.setSecure(s);
                return this;
            },
            setConf: function (c) {
                window.cookieStorage.setConf(c);
                return this;
            },
            setDefaultConf: function () {
                window.cookieStorage.setDefaultConf();
                return this;
            }
        });
    }

    // Get a new API on a namespace
    apis.initNamespaceStorage = function (ns) {
        return _createNamespace(ns);
    };
    if (storage_available) {
        // localStorage API
        apis.localStorage = _extend({}, storage, {_type: 'localStorage'});
        // sessionStorage API
        apis.sessionStorage = _extend({}, storage, {_type: 'sessionStorage'});
    } else {
        // localStorage API
        apis.localStorage = _extend({}, storage, {_type: 'localCookieStorage'});
        // sessionStorage API
        apis.sessionStorage = _extend({}, storage, {_type: 'sessionCookieStorage'});
    }
    // List of all namespace storage
    apis.namespaceStorages = {};
    // Remove all items in all storages
    apis.removeAllStorages = function (reinit_ns) {
        apis.localStorage.removeAll(reinit_ns);
        apis.sessionStorage.removeAll(reinit_ns);
        if (apis.cookieStorage) {
            apis.cookieStorage.removeAll(reinit_ns);
        }
        if (!reinit_ns) {
            apis.namespaceStorages = {};
        }
    };
    // About alwaysUseJson
    // By default, all values are string on html storages and the plugin don't use json to store simple values (strings, int, float...)
    // So by default, if you do storage.setItem('test',2), value in storage will be "2", not 2
    // If you set this property to true, all values set with the plugin will be stored as json to have typed values in any cases
    apis.alwaysUseJsonInStorage = function (value) {
        storage.alwaysUseJson = value;
        apis.localStorage.alwaysUseJson = value;
        apis.sessionStorage.alwaysUseJson = value;
        if (apis.cookieStorage) {
            apis.cookieStorage.alwaysUseJson = value;
        }
    };

    return apis;
}));
