/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module NativeApp
 */
import { join, relative } from "node:path";
import { DbResult, IModelStatus } from "@itwin/core-bentley";
import { IModelError } from "@itwin/core-common";
import { ECDb, ECDbOpenMode } from "./ECDb";
import { IModelHost } from "./IModelHost";
import { IModelJsFs } from "./IModelJsFs";
import { NativeHost } from "./NativeHost";
// cspell:ignore ecdb
/**
 * A local file stored in the [[NativeHost.appSettingsCacheDir]] for storing key/value pairs.
 * @public
 */
export class NativeAppStorage {
    _ecdb;
    id;
    static _ext = ".settings-db";
    static _storages = new Map();
    static _init = false;
    constructor(_ecdb, id) {
        this._ecdb = _ecdb;
        this.id = id;
    }
    /** Set the value for a key */
    setData(key, value) {
        const rc = this._ecdb.withPreparedSqliteStatement("INSERT INTO app_setting(key,type,val)VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET type=excluded.type,val=excluded.val", (stmt) => {
            let valType = (value === undefined || value === null) ? "null" : typeof value;
            if (valType === "object" && (value instanceof Uint8Array))
                valType = "Uint8Array";
            switch (valType) {
                case "null":
                case "number":
                case "string":
                case "boolean":
                case "Uint8Array":
                    break;
                default:
                    throw new IModelError(DbResult.BE_SQLITE_ERROR, `Unsupported type ${valType} for value for key='${key}`);
            }
            stmt.bindValue(1, key);
            stmt.bindValue(2, valType);
            stmt.bindValue(3, value);
            return stmt.step();
        });
        if (rc !== DbResult.BE_SQLITE_DONE)
            throw new IModelError(rc, "SQLite error");
        this._ecdb.saveChanges();
    }
    /** Get the value for a key from this Storage. If key is not present or is null, return undefined. */
    getData(key) {
        return this._ecdb.withPreparedSqliteStatement("SELECT type,val FROM app_setting WHERE key=?", (stmt) => {
            stmt.bindValue(1, key);
            if (DbResult.BE_SQLITE_ROW !== stmt.step())
                return undefined;
            const valType = stmt.getValueString(0);
            switch (valType) {
                case "number":
                    return stmt.getValueDouble(1);
                case "string":
                    return stmt.getValueString(1);
                case "boolean":
                    return Boolean(stmt.getValueInteger(1));
                case "Uint8Array":
                    return stmt.getValueBlob(1);
                case "null":
                    return undefined;
            }
            throw new IModelError(DbResult.BE_SQLITE_ERROR, `Unsupported type in cache ${valType}`);
        });
    }
    /** return the type of the value for a key, or undefined if not present. */
    getValueType(key) {
        return this._ecdb.withSqliteStatement("SELECT type FROM app_setting WHERE key=?", (stmt) => {
            stmt.bindValue(1, key);
            return stmt.step() === DbResult.BE_SQLITE_ROW ? stmt.getValueString(0) : undefined;
        });
    }
    /** return `true` if the key is present, but has a null value. */
    hasNullValue(key) {
        return this.getValueType(key) === "null";
    }
    /** Get the value for a key as a string. If it is not present, or not of type string, return undefined */
    getString(key) {
        const val = this.getData(key);
        return typeof val === "string" ? val : undefined;
    }
    /** Get the value for a key as a number. If it is not present, or not of type number, return undefined */
    getNumber(key) {
        const val = this.getData(key);
        return typeof val === "number" ? val : undefined;
    }
    /** Get the value for a key as a boolean. If it is not present, or not of type boolean, return undefined */
    getBoolean(key) {
        const val = this.getData(key);
        return typeof val === "boolean" ? val : undefined;
    }
    /** Get the value for a key as a Uint8Array. If it is not present, or not of type Uint8Array, return undefined */
    getUint8Array(key) {
        const val = this.getData(key);
        return val instanceof Uint8Array ? val : undefined;
    }
    /** Get all key names in this Storage */
    getKeys() {
        const keys = new Array();
        this._ecdb.withPreparedSqliteStatement("SELECT key FROM app_setting", (stmt) => {
            while (DbResult.BE_SQLITE_ROW === stmt.step()) {
                keys.push(stmt.getValueString(0));
            }
        });
        return keys;
    }
    /** Remove a key/value pair from this Storage */
    removeData(key) {
        const rc = this._ecdb.withPreparedSqliteStatement("DELETE FROM app_setting WHERE key=?", (stmt) => {
            stmt.bindValue(1, key);
            return stmt.step();
        });
        if (rc !== DbResult.BE_SQLITE_DONE) {
            throw new IModelError(rc, "SQLite error");
        }
    }
    /** Remove all key/value pairs */
    removeAll() {
        const rc = this._ecdb.withPreparedSqliteStatement("DELETE FROM app_setting", (stmt) => {
            return stmt.step();
        });
        if (rc !== DbResult.BE_SQLITE_DONE) {
            throw new IModelError(rc, "SQLite error");
        }
    }
    /** Close this Storage. */
    close(deleteFile = false) {
        const storageFile = join(NativeHost.appSettingsCacheDir, this.id);
        this._ecdb.saveChanges();
        this._ecdb.closeDb();
        this._ecdb = undefined;
        if (deleteFile)
            IModelJsFs.removeSync(storageFile);
        NativeAppStorage._storages.delete(this.id);
    }
    static init(ecdb) {
        return ecdb.withPreparedSqliteStatement("CREATE TABLE app_setting(key PRIMARY KEY,type,val);", (stmt) => {
            return stmt.step();
        });
    }
    /** find and open storage by its name. */
    static find(name) {
        const storage = this._storages.get(name);
        if (undefined === storage)
            throw new IModelError(IModelStatus.FileNotFound, `Storage ${name} not open`);
        return storage;
    }
    /** Close all opened Storages.
     * @internal
     */
    static closeAll() {
        this._storages.forEach((value) => value.close());
        this._storages.clear();
    }
    /** @internal */
    static getStorageNames() {
        return IModelJsFs.readdirSync(NativeHost.appSettingsCacheDir).filter((name) => name.endsWith(this._ext));
    }
    /** Open or find a Storage by name. */
    static open(name) {
        if (!this._init) {
            IModelHost.onBeforeShutdown.addOnce(() => this.closeAll());
            this._init = true;
        }
        if (name.includes("\x00"))
            throw new Error("Storage name contains illegal characters");
        const fileName = name + this._ext;
        if (!IModelJsFs.existsSync(NativeHost.appSettingsCacheDir))
            IModelJsFs.recursiveMkDirSync(NativeHost.appSettingsCacheDir);
        const storageFile = join(NativeHost.appSettingsCacheDir, fileName);
        if (relative(NativeHost.appSettingsCacheDir, storageFile).startsWith(".."))
            throw new Error("Storage name should not be a path");
        try {
            return this.find(fileName); // see if it's already open
        }
        catch {
            const ecdb = new ECDb();
            if (IModelJsFs.existsSync(storageFile)) {
                ecdb.openDb(storageFile, ECDbOpenMode.ReadWrite);
            }
            else {
                ecdb.createDb(storageFile);
                const rc = this.init(ecdb);
                if (rc !== DbResult.BE_SQLITE_DONE)
                    throw new IModelError(rc, "SQLite error");
                ecdb.saveChanges();
            }
            const storage = new NativeAppStorage(ecdb, fileName);
            this._storages.set(fileName, storage);
            return storage;
        }
    }
}
//# sourceMappingURL=NativeAppStorage.js.map