/*
 * Copyright (C) 2018 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

WI.ObjectStore = class ObjectStore
{
    constructor(name, options = {})
    {
        this._name = name;
        this._options = options;
    }

    // Static

    static supported()
    {
        return (!window.InspectorTest || WI.ObjectStore.__testObjectStore) && window.indexedDB;
    }

    static get _databaseName()
    {
        let inspectionLevel = InspectorFrontendHost ? InspectorFrontendHost.inspectionLevel() : 1;
        let levelString = (inspectionLevel > 1) ? "-" + inspectionLevel : "";
        return "com.apple.WebInspector" + levelString;
    }

    static _open(callback)
    {
        if (WI.ObjectStore._database) {
            callback(WI.ObjectStore._database);
            return;
        }

        if (Array.isArray(WI.ObjectStore._databaseCallbacks)) {
            WI.ObjectStore._databaseCallbacks.push(callback);
            return;
        }

        WI.ObjectStore._databaseCallbacks = [callback];

        const version = 1; // Increment this for every edit to `WI.objectStores`.

        let databaseRequest = indexedDB.open(WI.ObjectStore._databaseName, version);
        databaseRequest.addEventListener("upgradeneeded", (event) => {
            let database = databaseRequest.result;

            let objectStores = Object.values(WI.objectStores);
            if (WI.ObjectStore.__testObjectStore)
                objectStores.push(WI.ObjectStore.__testObjectStore);

            let existingNames = new Set;
            for (let objectStore of objectStores) {
                if (!database.objectStoreNames.contains(objectStore._name))
                    database.createObjectStore(objectStore._name, objectStore._options);

                existingNames.add(objectStore._name);
            }

            for (let objectStoreName of database.objectStoreNames) {
                if (!existingNames.has(objectStoreName))
                    database.deleteObjectStore(objectStoreName);
            }
        });
        databaseRequest.addEventListener("success", (successEvent) => {
            WI.ObjectStore._database = databaseRequest.result;
            WI.ObjectStore._database.addEventListener("close", (closeEvent) => {
                WI.ObjectStore._database = null;
            });

            for (let databaseCallback of WI.ObjectStore._databaseCallbacks)
                databaseCallback(WI.ObjectStore._database);

            WI.ObjectStore._databaseCallbacks = null;
        });
    }

    // Public

    associateObject(object, key, value)
    {
        if (typeof value === "object")
            value = this._resolveKeyPath(value, key).value;

        let resolved = this._resolveKeyPath(object, key);
        resolved.object[resolved.key] = value;
    }

    async getAll(...args)
    {
        if (!WI.ObjectStore.supported())
            return undefined;

        return this._operation("readonly", (objectStore) => objectStore.getAll(...args));
    }

    async add(...args)
    {
        if (!WI.ObjectStore.supported())
            return undefined;

        return this._operation("readwrite", (objectStore) => objectStore.add(...args));
    }

    async addObject(object, ...args)
    {
        if (!WI.ObjectStore.supported())
            return undefined;

        console.assert(typeof object.toJSON === "function", "ObjectStore cannot store an object without JSON serialization", object.constructor.name);
        let result = await this.add(object.toJSON(WI.ObjectStore.toJSONSymbol), ...args);
        this.associateObject(object, args[0], result);
        return result;
    }

    async delete(...args)
    {
        if (!WI.ObjectStore.supported())
            return undefined;

        return this._operation("readwrite", (objectStore) => objectStore.delete(...args));
    }

    async deleteObject(object, ...args)
    {
        if (!WI.ObjectStore.supported())
            return undefined;

        return this.delete(this._resolveKeyPath(object).value, ...args);
    }

    async clear(...args)
    {
        if (!WI.ObjectStore.supported())
            return undefined;

        return this._operation("readwrite", (objectStore) => objectStore.clear(...args));
    }

    // Private

    _resolveKeyPath(object, keyPath)
    {
        keyPath = keyPath || this._options.keyPath || "";

        let parts = keyPath.split(".");
        let key = parts.splice(-1, 1);
        while (parts.length) {
            if (!object.hasOwnProperty(parts[0]))
                break;
            object = object[parts.shift()];
        }

        if (parts.length)
            key = parts.join(".") + "." + key;

        return {
            object,
            key,
            value: object[key],
        };
    }

    async _operation(mode, func)
    {
        // IndexedDB transactions will auto-close if there are no active operations at the end of a
        // microtask, so we need to do everything using event listeners instead of promises.
        return new Promise((resolve, reject) => {
            WI.ObjectStore._open((database) => {
                let transaction = database.transaction([this._name], mode);
                let objectStore = transaction.objectStore(this._name);
                let request = null;

                try {
                    request = func(objectStore);
                } catch (e) {
                    reject(e);
                    return;
                }

                function listener(event) {
                    transaction.removeEventListener("complete", listener);
                    transaction.removeEventListener("error", listener);
                    request.removeEventListener("success", listener);
                    request.removeEventListener("error", listener);

                    if (request.error) {
                        reject(request.error);
                        return;
                    }

                    resolve(request.result);
                }
                transaction.addEventListener("complete", listener, {once: true});
                transaction.addEventListener("error", listener, {once: true});
                request.addEventListener("success", listener, {once: true});
                request.addEventListener("error", listener, {once: true});
            });
        });
    }
};

WI.ObjectStore._database = null;
WI.ObjectStore._databaseCallbacks = null;

WI.ObjectStore.toJSONSymbol = Symbol("ObjectStore-toJSON");

// Be sure to update the `version` above when making changes.
WI.objectStores = {
    audits: new WI.ObjectStore("audit-manager-tests", {keyPath: "__id", autoIncrement: true}),
};
