/**
 * Cesium - https://github.com/CesiumGS/cesium
 *
 * Copyright 2011-2020 Cesium Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Columbus View (Pat. Pend.)
 *
 * Portions licensed separately.
 * See https://github.com/CesiumGS/cesium/blob/master/LICENSE.md for full licensing details.
 */
define(['./when-8d13db60', './createTaskProcessorWorker'], function (when, createTaskProcessorWorker) { 'use strict';

    /**
         * @exports defined
         *
         * @param {*} value The object.
         * @returns {Boolean} Returns true if the object is defined, returns false otherwise.
         *
         * @example
         * if (Cesium.defined(positions)) {
         *      doSomething();
         * } else {
         *      doSomethingElse();
         * }
         */
        function defined(value) {
            return value !== undefined && value !== null;
        }

    /**
         * Constructs an exception object that is thrown due to a developer error, e.g., invalid argument,
         * argument out of range, etc.  This exception should only be thrown during development;
         * it usually indicates a bug in the calling code.  This exception should never be
         * caught; instead the calling code should strive not to generate it.
         * <br /><br />
         * On the other hand, a {@link RuntimeError} indicates an exception that may
         * be thrown at runtime, e.g., out of memory, that the calling code should be prepared
         * to catch.
         *
         * @alias DeveloperError
         * @constructor
         * @extends Error
         *
         * @param {String} [message] The error message for this exception.
         *
         * @see RuntimeError
         */
        function DeveloperError(message) {
            /**
             * 'DeveloperError' indicating that this exception was thrown due to a developer error.
             * @type {String}
             * @readonly
             */
            this.name = 'DeveloperError';

            /**
             * The explanation for why this exception was thrown.
             * @type {String}
             * @readonly
             */
            this.message = message;

            //Browsers such as IE don't have a stack property until you actually throw the error.
            var stack;
            try {
                throw new Error();
            } catch (e) {
                stack = e.stack;
            }

            /**
             * The stack trace of this exception, if available.
             * @type {String}
             * @readonly
             */
            this.stack = stack;
        }

        if (defined(Object.create)) {
            DeveloperError.prototype = Object.create(Error.prototype);
            DeveloperError.prototype.constructor = DeveloperError;
        }

        DeveloperError.prototype.toString = function() {
            var str = this.name + ': ' + this.message;

            if (defined(this.stack)) {
                str += '\n' + this.stack.toString();
            }

            return str;
        };

        /**
         * @private
         */
        DeveloperError.throwInstantiationError = function() {
            throw new DeveloperError('This function defines an interface and should not be called directly.');
        };

    var DataStatus = {
        NONE : 0,
        STORING : 1,
        STORED : 2,
        FAILED : 3
    };

    function IndexedDBScheduler(options) {
        if(!defined(options.name)){
            throw new DeveloperError('options.name is required.');
        }
        var defer = when.when.defer();
        this.dbname = options.name;
        var request = indexedDB.open(this.dbname);

        var that = this;
        request.onsuccess = function (event) {
            that.db = event.target.result;
            that.version = that.db.version;

            if (!defined(that.cachestatus)) {
                that.cachestatus = {

                };
            }
            defer.resolve(that);
        };

        request.onupgradeneeded = function (event) {
            that.db = event.target.result;
            that.version = that.db.version;
            defer.resolve(that);
        };

        request.onerror = function (event) {
            that.db = null;
            defer.reject('create database fail, error code : ' + event.target.errorcode);
        };

        this.layer = options.layer || null;
        this.storageType = options.storageType || "arrayBuffer";
        this.creatingTable = false;
        this.cachestatus = {};
        return defer.promise;
    }

    IndexedDBScheduler.prototype.checkObjectStoreExit = function(storeName) {
        if (!defined(this.db)) {
            return false;
        }

        return this.db.objectStoreNames.contains(storeName);
    };

    IndexedDBScheduler.prototype.createObjectStore = function(storeName) {
        var defer = when.when.defer();

        //防止多个请求同时创建同一张表
        if (!this.creatingTable) {
            if (this.db.objectStoreNames.contains(storeName)) {
                defer.reject(false);
                return defer.promise;
            }
            this.creatingTable = true;

            var that = this;

            //只能在onupgradeneeded方法中创建新表,所以需要用更高版本打开数据库触发更新
            var version = parseInt(that.db.version);
            that.db.close();
            var secondRequest = indexedDB.open(that.dbname, version + 1);
            secondRequest.onupgradeneeded  = function(e) {
                var database = e.target.result;
                that.db = database;
                var objectStore = database.createObjectStore(storeName, { keyPath : 'id'});
                if (defined(objectStore)) {
                    objectStore.createIndex('value', 'value', { unique : false});
                    that.creatingTable = false;
                    //初始化表的缓存状态
                    if (!defined(that.cachestatus)) {
                        that.cachestatus = {};
                    }

                    that.cachestatus[storeName] = {

                    };

                    //这边创建成功表后直接使用会出现各种奇怪的问题,索性关闭数据库后再打开
                    that.db.close();
                    var thirdRequest = indexedDB.open(that.dbname);

                    thirdRequest.onsuccess = function(e) {
                        var database = e.target.result;
                        that.db = database;
                        defer.resolve(true);
                    };
                } else {
                    that.creatingTable = false;
                    defer.reject(false);
                }
            };

            secondRequest.onsuccess = function(e) {
                e.target.result.close();
                defer.resolve(true);
            };

            secondRequest.onerror = function(e) {
                that.creatingTable = false;
                defer.reject(false);
            };
        } else {
            defer.reject(false);
        }

        return defer.promise;
    };

    IndexedDBScheduler.prototype.putElementInDB = function (table, key, data, objectArray) {
        var defer = when.when.defer();

        if (!defined(this.db)) {
            defer.reject(false);
            return defer.promise;
        }

        var objectStore;
        var that = this;

        //先检查缓存,如果已经写了就不去写了
        if (defined(that.cachestatus[table]) && !defined(objectArray)) {
            if (defined(that.cachestatus[table][key])) {
                if (that.cachestatus[table][key] === DataStatus.STORING || that.cachestatus[table][key] === DataStatus.STORED) {
                    defer.resolve(false);
                    return defer.promise;
                }
            }
        }

        //没有表,去创建指定名称的表
        if (!this.db.objectStoreNames.contains(table)) {
            this.createObjectStore(table).then(function(success) {
                var transaction = that.db.transaction([table], "readwrite");
                objectStore = transaction.objectStore(table);

                if (defined(objectArray)) {
                    for (var i = 0, len = objectArray.length; i < len; i++) {
                        objectStore.add({ id : objectArray[i].key, value : objectArray[i].value});
                    }

                    defer.resolve(true);
                } else {
                    var request = objectStore.add({ id : key, value : data});
                    request.onsuccess = function (event) {
                        defer.resolve(true);
                    };

                    request.onerror = function (event) {
                        defer.reject(false);
                    };
                }
            }, function(error) {
                defer.reject(false);
            });
        } else {
            if (!defined(that.cachestatus[table])) {
                that.cachestatus[table] = {

                };
            }

            //打开表并添加对应数据
            var transaction;
            try {
                transaction = this.db.transaction([table], "readwrite");
            } catch (e) {
                defer.reject(null);
                return defer.promise;
            }

            objectStore = transaction.objectStore(table);

            if (defined(objectArray)) {
                if (objectArray instanceof Array) {
                    for (var i = 0, len = objectArray.length; i < len; i++) {
                        if (that.cachestatus[table][objectArray[i].key] !== DataStatus.STORED) {
                            objectStore.add({ id : objectArray[i].key, value : objectArray[i].value});
                            that.cachestatus[table][objectArray[i].key] = DataStatus.STORED;
                        }
                    }
                    defer.resolve(true);
                } else {
                    for (var key in objectArray) {
                        if (isNaN(key * 1)) {
                            continue;
                        }
                        objectStore.add({ id : key, value : objectArray[key]});
                    }
                    defer.resolve(true);
                }
                
            } else if (!defined(key) || !defined(data)) {
                return;
            } else if (key instanceof Array && data instanceof Array) {
                for (var i = 0, len = key.length; i < len; i++) {
                    if (that.cachestatus[table][key[i]] !== DataStatus.STORED) {
                        objectStore.add({ id : key[i], value : data[i]});
                        that.cachestatus[table][key[i]] = DataStatus.STORED;
                    }
                }

                transaction.oncomplete = function(e) {
                    defer.resolve(true);
                };

                transaction.onerror = function(e) {
                    defer.reject(false);
                };
            } else {
                var request = objectStore.add({ id : key, value : data});
                that.cachestatus[table][key] = DataStatus.STORING;
                request.onsuccess = function (event) {
                    that.cachestatus[table][key] = DataStatus.STORED;
                    defer.resolve(true);
                };

                request.onerror = function (event) {
                    that.cachestatus[table][key] = DataStatus.FAILED;
                    defer.reject(false);
                };
            }
        }

        return defer.promise;
    };

    IndexedDBScheduler.prototype.getRangeFromDB = function(table, idRange) {
        var defer = when.when.defer();

        if (!defined(this.db)) {
            return null;
        }

        if (!this.db.objectStoreNames.contains(table)) {
            return null;
        }

        var transaction;
        try {
            transaction = this.db.transaction([table]);
        } catch (e) {
            defer.reject(null);
            return defer.promise;
        }

        var objectStore;
        try {
            objectStore = transaction.objectStore(table);
        } catch (e) {
            defer.reject(null);
        }

        var request = objectStore.openCursor(IDBKeyRange.bound(idRange[0], idRange[1]));

        var rs = [];
        request.onsuccess = function(event) {
            var cursor = event.target.result;
            if (!defined(cursor)) {
                defer.resolve(rs);
            } else {
                rs.push(cursor.value);
                cursor.continue();
            }
        };

        request.onerror = function(event) {
            defer.reject(null);
        };

        return defer.promise;
    };

    IndexedDBScheduler.prototype.getElementFromDB = function (table, key) {
        var defer = when.when.defer();

        if (!defined(this.db)) {
            return null;
        }

        if (!this.db.objectStoreNames.contains(table)) {
            return null;
        }

        var transaction;
        try {
            transaction = this.db.transaction([table]);
        } catch (e) {
            defer.reject(null);
            return defer.promise;
        }

        var objectStore;
        try {
            objectStore = transaction.objectStore(table);
        } catch (e) {
            defer.reject(null);
        }

        var request = objectStore.get(key);

        request.onsuccess = function(event) {
            if (!defined(event.target.result)) {
                defer.reject(null);
                return;
            }

            defer.resolve(event.target.result.value);
        };

        request.onerror = function(event) {
            defer.reject(null);
        };

        return defer.promise;
    };

    IndexedDBScheduler.prototype.getAllElementFromDB = function (table) {
        var defer = when.when.defer();

        if (!defined(this.db)) {
            return null;
        }

        if (!this.db.objectStoreNames.contains(table)) {
            return null;
        }

        var transaction;
        if (this.transaction != null) {
            transaction = this.transaction;
        } else {
            try {
                transaction = this.db.transaction([table]);
            } catch (e) {
                defer.reject(null);
                return defer.promise;
            }
        }
        

        var objectStore;
        try {
            objectStore = transaction.objectStore(table);
        } catch (e) {
            defer.reject(null);
        }

        var request = objectStore.getAll();

        request.onsuccess = function(event) {
            if (!defined(event.target.result)) {
                defer.reject(null);
                return;
            }

            defer.resolve(event.target.result);
        };

        request.onerror = function(event) {
            defer.reject(null);
        };

        return defer.promise;
    };

    IndexedDBScheduler.prototype.updateElementInDB = function (table, key, value, append) {
        var defer = when.when.defer();

        if (!defined(this.db)) {
            defer.resolve(false);
            return defer.promise;
        }

        if (!this.db.objectStoreNames.contains(table)) {
            defer.resolve(false);
            return defer.promise;
        }

        var transaction = this.db.transaction([table], "readwrite");
        var objectStore;
        try {
            objectStore = transaction.objectStore(table);
        } catch (e) {
            defer.resolve(false);
        }

        var request = objectStore.get(key);

        request.onsuccess = function(event) {
            var data = event.target.result;
            if (!defined(data)) {
                data = {
                    id : key
                };
            }

            if (append === true) {
                data.value = Object.assign(data.value, value);
            } else {
                data.value = value;
            }
            

            var requestUpdate = objectStore.put(data);
            requestUpdate.onsuccess = function(event) {
                defer.resolve(true);
            };

            requestUpdate.onerror = function(event) {
                defer.resolve(false);
            };
        };

        request.onerror = function(event) {
            defer.resolve(false);
        };

        return defer.promise;
    };

    IndexedDBScheduler.prototype.removeElementFromDB = function (table, key) {
        var defer = when.when.defer();

        if (!defined(this.db)) {
            defer.resolve(false);
            return defer.promise;
        }

        if (!this.db.objectStoreNames.contains(table)) {
            defer.resolve(false);
            return defer.promise;
        }

        var transaction = this.db.transaction([table], "readwrite");
        var objectStore;
        try {
            objectStore = transaction.objectStore(table);
        } catch (e) {
            defer.resolve(false);
        }

        var request = objectStore.delete(key);

        request.onerror = function(event) {
            defer.resolve(false);
        };

        request.onsuccess = function(event) {
            defer.resolve(true);
        };

        return defer.promise;
    };

    IndexedDBScheduler.prototype.clear = function(table) {
        var defer = when.when.defer();

        if (!defined(this.db)) {
            defer.resolve(false);
            return defer.promise;
        }

        if (!this.db.objectStoreNames.contains(table)) {
            defer.resolve(false);
            return defer.promise;
        }

        var transaction = this.db.transaction([table], "readwrite");
        var objectStore;
        try {
            objectStore = transaction.objectStore(table);
        } catch (e) {
            defer.resolve(false);
        }

        var request = objectStore.clear();

        request.onerror = function(event) {
            defer.resolve(false);
        };

        request.onsuccess = function(event) {
            defer.resolve(true);
        };

        return defer.promise;
    };

    var cache = {};

    function indexedDBWorker(parameters, transferableObjects) {
        var blob = parameters.blob;
        var key = parameters.key;
        var tablename = parameters.tablename;
        var dbname = parameters.dbname;
        var cacheSize = parameters.cacheSize || 100;

        var cahceKey = dbname + tablename;
        if (typeof cache[cahceKey] === "undefined") {
            cache[cahceKey] = {
                cache: [],
                creatingDB: false,
                scheduler: null,
                creatingTable: false
            };
        } else {
            cache[cahceKey].cache.push({
                key: key,
                value: blob
            });
        }


        if (cache[cahceKey].cache.length > cacheSize) {
            putElementInDB(dbname, tablename, cache[cahceKey]);
        }
    }

    function putElementInDB(dbname, tablename, cache) {
        if (cache.scheduler === null) {
            if (!cache.creatingDB) {
                cache.creatingDB = true;
                new IndexedDBScheduler({ name: dbname }).then(function (result) {
                    cache.creatingDB = false;
                    cache.scheduler = result;
                    if (!result.checkObjectStoreExit(tablename)) {
                        if (!cache.creatingTable) {
                            cache.creatingTable = true;
                            result.createObjectStore(tablename).then(function () {
                                cache.creatingTable = false;
                                result.putElementInDB(tablename, null, null, cache.cache);
                                cache.cache = [];
                            });
                        }
                    } else {
                        result.putElementInDB(tablename, null, null, cache.cache);
                        cache.cache = [];
                    }
                });
            }

        } else {
            if (!cache.scheduler.checkObjectStoreExit(tablename)) {
                if (!cache.creatingTable) {
                    cache.creatingTable = true;
                    cache.scheduler.createObjectStore(tablename).then(function () {
                        cache.creatingTable = false;
                        cache.scheduler.putElementInDB(tablename, null, null, cache.cache);
                        cache.cache = [];
                    });
                }
            } else {
                cache.scheduler.putElementInDB(tablename, null, null, cache.cache);
                cache.cache = [];
            }
        }

    }

    var indexedDBWorker$1 = createTaskProcessorWorker(indexedDBWorker);

    return indexedDBWorker$1;

});
