/*
 * wq.app v1.1.2 - @wq/store 1.1.2
 * Locally-persistent, optionally server-populated JSON datastore(s)
 * (c) 2012-2019, S. Andrew Sheppard
 * https://wq.io/license
 */

define(['localforage', 'localforage-memoryStorageDriver'], function (localForage, memoryStorageDriver) { 'use strict';

localForage = localForage && localForage.hasOwnProperty('default') ? localForage['default'] : localForage;
memoryStorageDriver = memoryStorageDriver && memoryStorageDriver.hasOwnProperty('default') ? memoryStorageDriver['default'] : memoryStorageDriver;

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }

  return obj;
}

function _objectSpread(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i] != null ? arguments[i] : {};
    var ownKeys = Object.keys(source);

    if (typeof Object.getOwnPropertySymbols === 'function') {
      ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
        return Object.getOwnPropertyDescriptor(source, sym).enumerable;
      }));
    }

    ownKeys.forEach(function (key) {
      _defineProperty(target, key, source[key]);
    });
  }

  return target;
}

var _stores = {}; // Hybrid module object provides/is a singleton instance...

var store = new _Store('main'); // ... and a way to retrieve/autoinit other stores

store.getStore = function (name) {
    if (_stores[name]) {
        return _stores[name];
    } else {
        return new _Store(name);
    }
}; // Internal variables and functions


var _verbosity = {
    Network: 1,
    Lookup: 2,
    Values: 3
};
localForage.defineDriver(memoryStorageDriver);

function _Store(name) {
    if (_stores[name]) {
        throw name + ' store already exists!';
    }

    var self = _stores[name] = this;
    self.name = name; // Base URL of web service

    self.service = undefined;
    self.debug = false; // Default parameters (e.g f=json)

    self.defaults = {};
    var _promises = {}; // Save promises to prevent redundant fetches

    self.init = function (opts) {
        var optlist = ['service', 'defaults', 'parseData', 'storageFail', 'fetchFail', // 'jsonp',  # FIXME: Restore or deprecate
        'ajax', 'debug', 'formatKeyword', 'alwaysExtractBlobs', 'getBlob', 'setBlob', 'removeBlob', 'makeRef'];
        optlist.forEach(function (opt) {
            if (opts.hasOwnProperty(opt)) {
                self[opt] = opts[opt];
            }
        });

        if (self.debug) {
            for (var level in _verbosity) {
                if (self.debug >= _verbosity[level]) {
                    self['debug' + level] = true;
                }
            }
        }

        self.ready = new Promise(_ready);
    };

    self.ready = {
        then: function () {
            throw 'Call init first!';
        }
    }; // Get value from datastore

    self.get = function (query) {
        if (Array.isArray(query)) {
            var promises = query.map(self.get);
            return Promise.all(promises);
        }

        query = self.normalizeQuery(query);
        var key = self.toKey(query);

        if (self.debugLookup) {
            console.log('looking up ' + key);
        } // Check storage first


        var promise = self.lf.getItem(key).then(function (result) {
            if (!result) {
                return fetchData();
            }

            if (self.debugLookup) {
                console.log('in storage');
            }

            return result;
        }, function () {
            return fetchData();
        });

        function fetchData() {
            // Search ends here if query is a simple string
            if (typeof query == 'string') {
                if (self.debugLookup) {
                    console.log('not found');
                }

                return null;
            } // More complex queries are assumed to be server requests


            return self.fetch(query, true);
        }

        return promise;
    }; // Set value


    self.set = function (query, value) {
        var key = self.toKey(query);

        if (value === null) {
            if (self.debugLookup) {
                console.log('deleting ' + key);
            }

            return self.lf.removeItem(key);
        } else {
            if (self.debugLookup) {
                console.log('saving new value for ' + key);

                if (self.debugValues) {
                    console.log(value);
                }
            }

            return self.lf.setItem(key, value).then(function (d) {
                return d;
            }, function (err) {
                return self.storageFail(value, err);
            });
        }
    }; // Callback for localStorage failure - override to inform the user


    self.storageFail = function (item, error) {
        return self.storageUsage().then(function (usage) {
            var msg;

            if (usage > 0) {
                msg = 'Storage appears to be full.';
            } else {
                msg = 'Storage appears to be disabled.';
            }

            console.warn(msg + '  Caught Error:');
            console.warn(error && error.stack || error);
            throw new Error(msg);
        });
    };

    self.storageUsage = _globalStorageUsage; // Convert "/url" to {'url': "url"} (simplify common use case)

    self.normalizeQuery = function (query) {
        if (typeof query === 'string' && query.charAt(0) == '/') {
            query = {
                url: query.replace(/^\//, '')
            };
        }

        return query;
    }; // Helper to allow simple objects to be used as keys


    self.toKey = function (query) {
        query = self.normalizeQuery(query);

        if (!query) {
            throw 'Invalid query!';
        }

        if (typeof query == 'string') {
            return query;
        } else {
            return new URLSearchParams(query).toString();
        }
    }; // Helper to check existence of a key without loading the object


    self.exists = function (query) {
        var key = self.toKey(query);
        return self.keys().then(function (keys) {
            var found = false;
            keys.forEach(function (k) {
                if (k === key) {
                    found = true;
                }
            });
            return found;
        });
    }; // Fetch data from server


    self.fetch = function (query, cache) {
        query = self.normalizeQuery(query);
        var key = self.toKey(query);

        var data = _objectSpread({}, self.defaults, query);

        var url = self.service;

        if (data.hasOwnProperty('url')) {
            url = url + '/' + data.url;
            delete data.url;
        }

        if (data.format && !self.formatKeyword) {
            url += '.' + data.format;
            delete data.format;
        }

        if (_promises[key]) {
            return _promises[key];
        }

        if (self.debugNetwork) {
            console.log('fetching ' + key);
        }

        var promise = self.ajax(url, data, 'GET');
        _promises[key] = promise.then(function (result) {
            var data = self.parseData(result);
            delete _promises[key];

            if (!data) {
                self.fetchFail(query, 'Error parsing data!');
                return;
            }

            if (self.debugNetwork) {
                console.log('received result for ' + key);

                if (self.debugValues) {
                    console.log(data);
                }
            }

            if (cache) {
                return self.set(query, data);
            } else {
                return data;
            }
        }, function (error) {
            delete _promises[key];
            console.error(error);
            self.fetchFail(query, 'Error parsing data!');
        });
        return _promises[key];
    }; // Hook to allow full AJAX customization


    self.ajax = function (url, data, method, headers) {
        var urlObj = new URL(url, window.location.origin);

        if (!method) {
            method = 'GET';
        } else {
            method = method.toUpperCase();
        }

        if (method == 'GET') {
            Object.entries(data || {}).forEach(function ([key, value]) {
                return urlObj.searchParams.append(key, value);
            });
            data = null;
        }

        return fetch(urlObj, {
            method: method,
            body: data,
            headers: headers
        }).then(function (response) {
            if (response.ok) {
                return response.json();
            } else {
                return response.text().then(function (result) {
                    var error = new Error();

                    try {
                        error.json = JSON.parse(result);
                    } catch (e) {
                        error.text = result;
                    }

                    error.status = response.status;
                    throw error;
                });
            }
        });
    }; // Callback for fetch() failures - override to inform the user


    self.fetchFail = function (query, error) {
        var key = self.toKey(query);
        console.warn('Error loading ' + key + ': ' + error);
    }; // Helper function for prefetching data


    self.prefetch = function (query) {
        return self.fetch(query, true);
    }; // Process service fetch() results
    // (override if response data is in a child node)


    self.parseData = function (result) {
        // Default: assume JSON root is actual data
        return result;
    }; // Clear local caches


    self.reset = function (all) {
        if (all) {
            // Clear out everything - will affect other stores!
            return _clearAll();
        } else {
            // Only clear items for this store
            return self.lf.clear();
        }
    }; // List storage keys matching this store's key prefix


    self.keys = function () {
        return self.lf.keys();
    };

    self.lf = {};
    ['getItem', 'setItem', 'removeItem', 'keys', 'clear'].forEach(function (key) {
        self.lf[key] = function () {
            var args = arguments;
            return self.ready.then(function () {
                return self.lf[key].apply(this, args);
            });
        };
    });

    function _ready(resolve) {
        var resolved = false;
        self.lf = localForage.createInstance({
            name: self.name
        });
        self.lf.ready().then(function () {
            self.lf.setItem('wq-store-test', true).then(function () {
                resolved = true;
                return self.lf.removeItem('wq-store-test');
            }).then(resolve, fallback); // localForage.ready() failed for some reason
        }, fallback);
        setTimeout(function () {
            if (!resolved) {
                // localForage failed, and also failed to reject() for some
                // reason - this should be rare but has happened in the wild.
                console.error('Storage failed to initialize in 5 seconds');
                fallback();
            }
        }, 5000);

        function fallback() {
            if (!resolved) {
                if (self.debug) {
                    console.warn('Offline storage is not working; using in-memory store');
                }

                self.lf.setDriver(memoryStorageDriver._driver).then(resolve);
            }

            resolved = true;
        }
    }
} // Simple computation for quota usage across stores


function _globalStorageUsage() {
    return Promise.all(Object.keys(_stores).map(function (storeName) {
        var lf = _stores[storeName].lf,
            keyPromise;

        try {
            keyPromise = lf.keys();
        } catch (e) {
            return 0;
        }

        keyPromise.then(function (keys) {
            var results = keys.map(function (key) {
                return lf.getItem(key).then(function (item) {
                    // FIXME: This won't handle binary values
                    return JSON.stringify(item).length;
                });
            });
            return Promise.all(results).then(function (lengths) {
                var usage = 0;
                lengths.forEach(function (l) {
                    usage += l;
                }); // UTF-16 means two bytes per character in storage
                // FIXME: Is this true for non-localStorage backends?

                return usage * 2;
            });
        });
    })).then(function (allResults) {
        var total = 0;
        allResults.forEach(function (result) {
            total += result;
        });
        return total;
    });
}

function _clearAll() {
    return Promise.all(Object.keys(_stores).map(function (storeName) {
        return _stores[storeName].reset();
    }));
}

return store;

});
