"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const lib_types_1 = require("@shardeum-foundation/lib-types");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const utils = __importStar(require("../utils"));
const logger_1 = require("../logger");
const schemaDefintions_1 = require("./utils/schemaDefintions");
const sqlOpertors_1 = require("./utils/sqlOpertors");
class BetterSqlite3Storage {
    // note that old storage passed in logger, now we pass in the specific log for it to use.  This works for application use, but may need to rethink if we apply this to shardus core
    constructor(models, storageConfig, logger, baseDir, profiler) {
        this.baseDir = baseDir;
        this.storageConfig = storageConfig;
        this.storageConfig.options.storage = path_1.default.join(this.baseDir, this.storageConfig.options.storage);
        this.profiler = profiler;
        // Setup logger
        this.mainLogger = logger.getLogger('main');
        // Start sqlite and load models
        this.initialized = false;
        this.storageModels = {};
        for (const [modelName, modelAttributes] of models) {
            this.sqlite3Define(modelName, modelAttributes);
        }
    }
    sqlite3Define(modelName, modelAttributes) {
        const tableName = modelName;
        const modelData = {
            tableName,
            columns: [],
            columnsString: '',
            substitutionString: '',
            isColumnJSON: {},
            JSONkeys: [],
        };
        for (const key in modelAttributes) {
            if (Object.prototype.hasOwnProperty.call(modelAttributes, key)) {
                modelData.columns.push(key);
                // eslint-disable-next-line security/detect-object-injection
                const value = modelAttributes[key];
                let type = value.type;
                if (!type) {
                    type = value;
                }
                if (type.toString() === schemaDefintions_1.SQLDataTypes.JSON.toString()) {
                    // eslint-disable-next-line security/detect-object-injection
                    modelData.isColumnJSON[key] = true;
                    modelData.JSONkeys.push(key);
                }
                else {
                    // eslint-disable-next-line security/detect-object-injection
                    modelData.isColumnJSON[key] = false;
                }
            }
        }
        for (let i = 0; i < modelData.columns.length; i++) {
            // eslint-disable-next-line security/detect-object-injection
            const key = modelData.columns[i];
            modelData.columnsString += key;
            modelData.substitutionString += '?';
            if (i < modelData.columns.length - 1) {
                modelData.columnsString += ', ';
                modelData.substitutionString += ', ';
            }
        }
        modelData.insertOrReplaceString = `INSERT OR REPLACE INTO ${modelData.tableName} (${modelData.columnsString} ) VALUES (${modelData.substitutionString})`;
        modelData.insertString = `INSERT INTO ${modelData.tableName} (${modelData.columnsString} ) VALUES (${modelData.substitutionString})`;
        modelData.selectString = `SELECT * FROM ${modelData.tableName} `;
        modelData.updateString = `UPDATE ${modelData.tableName} SET `;
        modelData.deleteString = `DELETE FROM ${modelData.tableName} `;
        // eslint-disable-next-line security/detect-object-injection
        this.storageModels[tableName] = modelData;
        // todo base this off of models
    }
    async init() {
        // Create dbDir if it doesn't exist
        const dbDir = path_1.default.parse(this.storageConfig.options.storage).dir;
        await _ensureExists(dbDir);
        this.mainLogger.info('Created Database directory.');
        if (this.storageConfig.options.memoryFile) {
            this.db = new better_sqlite3_1.default(':memory:');
        }
        else {
            this.db = new better_sqlite3_1.default(this.storageConfig.options.storage);
        }
        await this.run('PRAGMA synchronous = OFF');
        await this.run('PRAGMA journal_mode = MEMORY');
        this.initialized = true;
        this.mainLogger.info('Database initialized.');
    }
    async close() {
        this.db.close();
    }
    async runCreate(createStatement) {
        await this.run(createStatement);
    }
    _checkInit() {
        if (!this.initialized)
            throw new Error('Storage not initialized.');
    }
    _create(table, object, opts) {
        if (Array.isArray(object)) {
            // TODO transaciton or something else
            for (const subObj of object) {
                this._create(table, subObj, opts);
            }
            return;
        }
        let queryString = table.insertString;
        if (opts && opts.createOrReplace) {
            queryString = table.insertOrReplaceString;
        }
        const inputs = [];
        for (const column of table.columns) {
            // eslint-disable-next-line security/detect-object-injection
            let value = object[column];
            // eslint-disable-next-line security/detect-object-injection
            if (table.isColumnJSON[column]) {
                value = lib_types_1.Utils.safeStringify(value);
            }
            inputs.push(value);
        }
        queryString += this.options2string(opts);
        return this.run(queryString, inputs);
    }
    async _read(table, params, opts) {
        let queryString = table.selectString;
        const paramsArray = this.params2Array(params, table);
        const { whereString, whereValueArray } = this.paramsToWhereStringAndValues(paramsArray);
        const valueArray = whereValueArray;
        queryString += whereString;
        queryString += this.options2string(opts);
        return await this.all(queryString, valueArray);
    }
    _update(table, values, where, opts) {
        let queryString = table.updateString;
        const valueParams = this.params2Array(values, table);
        const { resultString, valueArray } = this.paramsToAssignmentStringAndValues(valueParams);
        queryString += resultString;
        const whereParams = this.params2Array(where, table);
        const { whereString, whereValueArray } = this.paramsToWhereStringAndValues(whereParams);
        queryString += whereString;
        const newValueArray = valueArray.concat(whereValueArray);
        queryString += this.options2string(opts);
        return this.run(queryString, newValueArray);
    }
    _delete(table, where, opts) {
        let queryString = table.deleteString;
        const whereParams = this.params2Array(where, table);
        const { whereString, whereValueArray } = this.paramsToWhereStringAndValues(whereParams);
        const valueArray = whereValueArray;
        queryString += whereString;
        queryString += this.options2string(opts);
        return this.run(queryString, valueArray);
    }
    _rawQuery(queryString, valueArray) {
        return this.all(queryString, valueArray);
    }
    params2Array(paramsObj, table) {
        if (paramsObj == null) {
            return [];
        }
        const paramsArray = [];
        for (const key in paramsObj) {
            if (Object.prototype.hasOwnProperty.call(paramsObj, key)) {
                const paramEntry = { name: key };
                // eslint-disable-next-line security/detect-object-injection
                const value = paramsObj[key];
                if (utils.isObject(value)) {
                    // WHERE column_name BETWEEN value1 AND value2;
                    if (value[sqlOpertors_1.Op.between]) {
                        const between = value[sqlOpertors_1.Op.between];
                        paramEntry.type = 'BETWEEN';
                        paramEntry.v1 = between[0];
                        paramEntry.v2 = between[1];
                        paramEntry.sql = `${paramEntry.name} ${paramEntry.type} ? AND ? `;
                        paramEntry.vals = [paramEntry.v1, paramEntry.v2];
                    }
                    // WHERE column_name IN (value1, value2, ...)
                    if (value[sqlOpertors_1.Op.in]) {
                        const inValues = value[sqlOpertors_1.Op.in];
                        paramEntry.type = 'IN';
                        let questionMarks = '';
                        for (let i = 0; i < inValues.length; i++) {
                            questionMarks += '?';
                            if (i < inValues.length - 1) {
                                questionMarks += ' , ';
                            }
                        }
                        paramEntry.sql = `${paramEntry.name} ${paramEntry.type} (${questionMarks})`;
                        paramEntry.vals = [];
                        paramEntry.vals = paramEntry.vals.concat(inValues);
                    }
                    if (value[sqlOpertors_1.Op.lte]) {
                        const rightHandValue = value[sqlOpertors_1.Op.lte];
                        paramEntry.type = 'LTE';
                        paramEntry.v1 = rightHandValue;
                        paramEntry.sql = `${paramEntry.name} <= ?`;
                        paramEntry.vals = [paramEntry.v1];
                    }
                    if (value[sqlOpertors_1.Op.gte]) {
                        const rightHandValue = value[sqlOpertors_1.Op.gte];
                        paramEntry.type = 'GTE';
                        paramEntry.v1 = rightHandValue;
                        // paramEntry.v2 = between[1]
                        paramEntry.sql = `${paramEntry.name} >= ?`;
                        paramEntry.vals = [paramEntry.v1];
                    }
                }
                else {
                    paramEntry.type = '=';
                    paramEntry.v1 = value;
                    paramEntry.sql = `${paramEntry.name} ${paramEntry.type} ?`;
                    if (table.isColumnJSON[paramEntry.name]) {
                        paramEntry.v1 = lib_types_1.Utils.safeStringify(paramEntry.v1);
                    }
                    paramEntry.vals = [paramEntry.v1];
                }
                paramsArray.push(paramEntry);
            }
        }
        return paramsArray;
    }
    paramsToWhereStringAndValues(paramsArray) {
        let whereValueArray = [];
        let whereString = '';
        for (let i = 0; i < paramsArray.length; i++) {
            if (i === 0) {
                whereString += ' WHERE ';
            }
            // eslint-disable-next-line security/detect-object-injection
            const paramEntry = paramsArray[i];
            whereString += '(' + paramEntry.sql + ')';
            if (i < paramsArray.length - 1) {
                whereString += ' AND ';
            }
            whereValueArray = whereValueArray.concat(paramEntry.vals);
        }
        return { whereString, whereValueArray };
    }
    paramsToAssignmentStringAndValues(paramsArray) {
        let valueArray = [];
        let resultString = '';
        for (let i = 0; i < paramsArray.length; i++) {
            // eslint-disable-next-line security/detect-object-injection
            const paramEntry = paramsArray[i];
            resultString += paramEntry.sql;
            if (i < paramsArray.length - 1) {
                resultString += ' , ';
            }
            valueArray = valueArray.concat(paramEntry.vals);
        }
        return { resultString, valueArray };
    }
    options2string(optionsObj) {
        if (optionsObj == null) {
            return '';
        }
        let optionsString = '';
        if (optionsObj.order) {
            optionsString += ' ORDER BY ';
            for (let i = 0; i < optionsObj.order.length; i++) {
                // eslint-disable-next-line security/detect-object-injection
                const orderEntry = optionsObj.order[i];
                optionsString += ` ${orderEntry[0]} ${orderEntry[1]} `;
                if (i < optionsObj.order.length - 1) {
                    optionsString += ',';
                }
            }
        }
        if (optionsObj.limit) {
            optionsString += ` LIMIT ${optionsObj.limit}`;
        }
        return optionsString;
    }
    // run/get/all promise wraps from this tutorial: https://stackabuse.com/a-sqlite-tutorial-with-node-js/
    run(sql, params = []) {
        return new Promise((resolve, reject) => {
            try {
                const { lastInsertRowid } = this.db.prepare(sql).run(params);
                resolve({ id: lastInsertRowid });
            }
            catch (err) {
                if (logger_1.logFlags.console)
                    console.log('Error running sql ' + sql);
                if (logger_1.logFlags.console)
                    console.log(err);
                reject(err);
            }
        });
    }
    get(sql, params = []) {
        return new Promise((resolve, reject) => {
            try {
                const result = this.db.prepare(sql).get(params);
                resolve(result);
            }
            catch (err) {
                if (logger_1.logFlags.console)
                    console.log('Error running sql: ' + sql);
                if (logger_1.logFlags.console)
                    console.log(err);
                reject(err);
            }
        });
    }
    all(sql, params = []) {
        return new Promise((resolve, reject) => {
            try {
                const rows = this.db.prepare(sql).all(params);
                resolve(rows);
            }
            catch (err) {
                if (logger_1.logFlags.console)
                    console.log('Error running sql: ' + sql);
                if (logger_1.logFlags.console)
                    console.log(err);
                reject(err);
            }
        });
    }
}
// From: https://stackoverflow.com/a/21196961
async function _ensureExists(dir) {
    return new Promise((resolve, reject) => {
        // only creates an empty directory and does nothing if it already exists
        // eslint-disable-next-line security/detect-non-literal-fs-filename
        fs_1.default.mkdir(dir, { recursive: true }, (err) => {
            if (err) {
                // Ignore err if folder exists
                if (err.code === 'EEXIST')
                    resolve();
                // Something else went wrong
                else
                    reject(err);
            }
            else {
                // Successfully created folder
                resolve();
            }
        });
    });
}
exports.default = BetterSqlite3Storage;
//# sourceMappingURL=betterSqlite3storage.js.map