"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv").config();
const Promise = require("bluebird");
const _ = require("lodash");
let connect;
const schemaValidator = new (require("./schemaValidation.js"))(connect);
// Let's get mongodb working first
/**
 * Create an new DynamicSchema instance
 *
 * @name DynamicSchema
 * @class
 */
class Schema {
    constructor() {
        /**
         * The name of the table.
         *
         * @name tableName
         * @type string
         * @memberOf DynamicSchema
         * @instance
         */
        this.tableName = null;
        /**
         * The slug of the table.
         *
         * @name tableSlug
         * @type string
         * @memberOf DynamicSchema
         * @instance
         */
        this.tableSlug = null;
        /**
         * The table's column definitions.
         *
         * @name definition
         * @type object
         * @memberOf DynamicSchema
         * @instance
         */
        this.definition = {};
        // Async setup, should be moved to a one time setup script
        // connect.then((db) => {
        // 	return db.collection("_schema").indexExists("_$id").then((result) => {
        // 		if(!result){
        // 			return Promise.resolve(db);
        // 		}
        // 	});
        // }).then((db) => {
        // 	return db.collection("_schema").createIndex("_$id", {
        // 		unique: true
        // 	});
        // });
    }
    /**
     * Create a new table with the given schema. Schema must adhere to the
     * JSON Schema definition set out in
     * [https://json-schema.org/](https://json-schema.org/)
     *
     * Each property corresponds to each column in the database. A few
     * custom attributes to each property can be included for use by
     * DynamicSchema to generate columns for special behaviour.
     *
     * These properties are:
     * - `isIndex`: Whether the column is an index field
     * - `isUnique`: Whether the column is an unique field
     * - `isAutoIncrement`: Whether the column is an auto-incrementing integer
     *
     * @method createTable
     * @memberOf DynamicSchema
     * @instance
     * @param {object} schema
     * @param {string} schema.$id - ID of the table, must be unique
     * @param {string} [schema.title] - Defaults to `schema.$id`
     * @param {object} schema.properties - The column definitions of the table
     * @return {Promise}
     */
    createTable(schema) {
        if (!schemaValidator.validate("rootSchema", schema)) {
            return Promise.reject(schemaValidator.errors);
        }
        const tableSlug = schema.$id;
        const tableName = schema.title || schema.$id;
        const columns = schema.properties;
        return connect.then((db) => {
            const promises = [];
            // NOTE: Do we need to check for existence first?
            promises.push(db.createCollection(tableSlug).then((col) => {
                this.tableName = tableName;
                this.tableSlug = tableSlug;
                return Promise.resolve(db);
            }));
            promises.push(db.createCollection("_counters").then((col) => {
                return col.indexExists("_$id").then((result) => {
                    if (result === false) {
                        return col.createIndex("_$id", { unique: true }).then(() => {
                            return Promise.resolve();
                        });
                    }
                    else {
                        return Promise.resolve();
                    }
                }).then(() => {
                    return col.insertOne({
                        _$id: tableSlug,
                        sequences: {}
                    }).then(() => {
                        return Promise.resolve(db);
                    });
                });
            }));
            const databaseInsert = {
                _$schema: schema.$schema,
                _$id: schema.$id,
                title: schema.title,
                description: schema.description,
                type: schema.type,
                properties: schema.properties,
                required: schema.required
            };
            promises.push(db.collection("_schema").insertOne(databaseInsert));
            this.definition = columns;
            promises.push(this._writeSchema());
            return Promise.all(promises);
        }).then(() => {
            // Handle index columns
            let promises = [];
            _.each(columns, (column, key) => {
                if (column.isIndex) {
                    promises.push(this.addIndex({
                        name: key,
                        unique: column.isUnique,
                        autoIncrement: column.isAutoIncrement
                    }));
                }
            });
            return Promise.all(promises);
        }).catch((err) => {
            this.tableName = null;
            this.tableSlug = null;
            throw Promise.reject(err);
        });
    }
    /**
     * Add an index to the table's schema
     *
     * @method renameTable
     * @memberOf DynamicSchema
     * @instance
     * @param {string} newSlug
     * @param {string} [newName] Defaults to newSlug
     * @return {Promise}
     */
    renameTable(newSlug, newName) {
        return connect.then((db) => {
            const promises = [];
            promises.push(db.collection("_schema").findOneAndUpdate({ "_$id": this.tableSlug }, {
                $set: {
                    "_$id": newSlug,
                    "title": newName || newSlug
                }
            }));
            promises.push(db.collection("_counters").findOneAndUpdate({ "_$id": this.tableSlug }, {
                $set: {
                    "_$id": newSlug
                }
            }));
            promises.push(db.renameCollection(this.tableSlug, newSlug));
            return Promise.all(promises);
        }).then(() => {
            this.tableSlug = newSlug;
            this.tableName = newName || newSlug;
            return Promise.resolve();
        });
    }
    /**
     * Add an index to the table's schema.
     *
     * @method addIndex
     * @memberOf DynamicSchema
     * @instance
     * @param {object} options
     * @param {string} options.name - The name of the column to be used as index
     * @param {boolean} [options.unique] - Whether the index is unique or not
     * @param {boolean} [options.autoInrement] - Whether it is an
     *                  auto-incrementing index or not. If true, `options.unique`
     *                  is automatically set to true
     * @return {Promise}
     */
    addIndex(options) {
        const columnName = options.name;
        const isAutoIncrement = options.autoIncrement;
        let unique = options.unique;
        if (isAutoIncrement && unique === false) {
            console.warn("Auto increment index must be unique, setting to unique.");
            unique = true;
        }
        if (typeof unique === "undefined") {
            unique = true;
        }
        return connect.then((db) => {
            return db.collection(this.tableSlug).createIndex(columnName, { unique: unique, name: columnName });
        }).then(() => {
            if (isAutoIncrement) {
                return this._setCounter(this.tableSlug, columnName);
            }
            else {
                return Promise.resolve();
            }
        }).catch((err) => {
            throw err;
        });
    }
    renameIndex(columnName, newColumnName) {
        // Maybe drop index then recreate but do consider why you need to do this
    }
    /**
     * Remove an index to the table's schema
     *
     * @method removeIndex
     * @memberOf DynamicSchema
     * @instance
     * @param {string} columnName - The name of the index to remove
     * @return {Promise}
     */
    removeIndex(columnName) {
        return connect.then((db) => {
            return db.collection(this.tableSlug).dropIndex(columnName)
                .then(() => {
                return Promise.resolve(db);
            });
        }).then((db) => {
            if (columnName === "_uid") {
                return db.collection("_counters").findOneAndDelete({
                    _$id: this.tableSlug
                });
            }
            else {
                return Promise.resolve();
            }
        }).catch((err) => {
            throw err;
        });
    }
    /**
     * Read the schema definition from the database.
     *
     * @method read
     * @memberOf DynamicSchema
     * @instance
     * @param {string} tableSlug - The name of the table schema to retrieve
     * @return {Promise} - Return promise, resolves to this object instance
     */
    read(tableSlug) {
        return connect.then((db) => {
            return db.collection("_schema").findOne({ _$id: tableSlug });
        }).then((data) => {
            if (data) {
                this.tableName = data.title;
                this.tableSlug = data._$id;
                this.definition = data.properties;
            }
            else {
                this.tableName = "";
                this.tableSlug = "";
                this.definition = {};
            }
            return Promise.resolve(this);
        }).catch((err) => {
            throw err;
        });
    }
    /**
     * Define the table's columns. Passed object must adhere to `properties`
     * attribute of [JSON Schema](https://json-schema.org/)'s definition.
     *
     * @method define
     * @memberOf DynamicSchema
     * @instance
     * @param {object} definition - Definition of the table columns
     * @return {Promise}
     */
    define(def) {
        const oldDef = this.definition;
        this.definition = def;
        // Create schema in RMDB, do nothing in NoSQL
        return connect.then((db) => {
            return db.collection("_schema").findOneAndUpdate({
                _$id: this.tableSlug,
            }, {
                $set: {
                    properties: def
                }
            }, {
                upsert: true
            });
        }).catch((err) => {
            this.definition = oldDef;
            throw err;
        });
    }
    /**
     * Add a single column to the table's schema definition.
     *
     * @method addColumn
     * @memberOf DynamicSchema
     * @instance
     * @param {string} name - The name of the column to add
     * @param {string} type - Type of the column to add
     * @param {string} [description] - Description of the column to add
     * @return {Promise}
     */
    addColumn(name, type, description = "") {
        if (!this.definition[name]) {
            this.definition[name] = {
                description: description,
                type: type
            };
        }
        else {
            // Column name already exist
            throw new Error("Column name already exist");
        }
        return this._writeSchema().catch((err) => {
            delete this.definition[name];
            throw err;
        });
    }
    /**
     * Add multiple columns to the table's schema definition.
     *
     * @method addColumns
     * @memberOf DynamicSchema
     * @instance
     * @param {object} definitions - Object of objects containing new columns
     *                               definitions
     * @return {Promise}
     */
    addColumns(def) {
        const oldDefinition = _.cloneDeep(this.definition);
        this.definition = _.assign(this.definition, def);
        return this._writeSchema().catch((err) => {
            this.definition = _.cloneDeep(oldDefinition);
            throw err;
        });
    }
    /**
     * Rename a single column in the table's schema definition.
     *
     * @method renameColumn
     * @memberOf DynamicSchema
     * @instance
     * @param {string} name - The name of the column to rename
     * @param {string} newName - The new name of the target column
     * @return {Promise}
     */
    renameColumn(name, newName) {
        this.definition[newName] = _.cloneDeep(this.definition[name]);
        delete this.definition[name];
        return connect.then((db) => {
            return this._writeSchema().then(() => {
                return db.collection("_counters").findOne({ "_$id": this.tableSlug });
            }).then((entry) => {
                if (entry) {
                    const sequences = _.cloneDeep(entry.sequences);
                    sequences[newName] = sequences[name];
                    delete sequences[name];
                    return db.collection("_counters").findOneAndUpdate({ "_$id": this.tableSlug }, {
                        $set: {
                            sequences: sequences
                        }
                    });
                }
                else {
                    return Promise.resolve();
                }
            }).catch((err) => {
                this.definition[name] = _.cloneDeep(this.definition[newName]);
                delete this.definition[newName];
                throw err;
            });
        });
    }
    /**
     * Change the type of a single column in the table's schema definition.
     *
     * @method changeColumnType
     * @memberOf DynamicSchema
     * @instance
     * @param {string} name - The name of the column to change type
     * @param {string} newType - The new type of the target column
     * @return {Promise}
     */
    changeColumnType(name, newType) {
        const oldType = this.definition[name].type;
        this.definition[name].type = newType;
        return this._writeSchema().catch((err) => {
            this.definition[name].type = oldType;
            throw err;
        });
    }
    /**
     * Remove a single column from the table's schema definition.
     *
     * @method removeColumn
     * @memberOf DynamicSchema
     * @instance
     * @param {string} name - The name of the column to remove
     * @return {Promise}
     */
    removeColumn(name) {
        const deleted = _.cloneDeep(this.definition[name]);
        delete this.definition[name];
        return this._writeSchema().catch((err) => {
            this.definition[name] = deleted;
            throw err;
        });
    }
    // Utils --------------------------------------------------------
    /**
     * Update the new schema structure into the database
     *
     * @method _writeSchema
     * @memberOf DynamicSchema
     * @instance
     * @private
     * @return {Promise}
     */
    _writeSchema() {
        return connect.then((db) => {
            return db.collection("_schema").findOneAndUpdate({ _$id: this.tableSlug }, {
                $set: {
                    properties: this.definition
                }
            });
        });
    }
    /**
     * Set an autoincrementing field to the _counters table (MongoDB only)
     *
     * @method _setCounter
     * @memberOf DynamicSchema
     * @instance
     * @private
     * @param {string} collection - The slug of the collection to set
     * @param {string} columnLabel - The slug of the column set as an autoincrementing index
     * @return {Promise}
     */
    _setCounter(collection, columnLabel) {
        return connect.then((db) => {
            const sequenceKey = `sequences.${columnLabel}`;
            return db.collection("_counters").findOneAndUpdate({
                _$id: collection
            }, {
                $set: {
                    [sequenceKey]: 0
                }
            });
        });
    }
    /**
     * Increment an autoincrementing index (MongoDB only)
     *
     * @method _setCounter
     * @memberOf DynamicSchema
     * @instance
     * @private
     * @param {string} collection - The slug of the collection to target
     * @param {string} columnLabel - The slug of the autoincrementing column
     * to increment
     * @return {Promise} - Promise of the next number in the sequence
     */
    _incrementCounter(collection, columnLabel) {
        return connect.then((db) => {
            return db.collection("_counters").findOne({
                _$id: collection
            }).then((result) => {
                const newSequence = result.sequences[columnLabel] + 1;
                const sequenceKey = `sequences.${columnLabel}`;
                return db.collection("_counters").findOneAndUpdate({
                    _$id: collection
                }, {
                    $set: {
                        [sequenceKey]: newSequence
                    }
                }).then(() => {
                    return Promise.resolve(newSequence);
                });
            });
        });
    }
    _validate() {
        // Validate database schema with this.definition
        // Return boolean
    }
}
module.exports = function (connection) {
    connect = connection;
    return Schema;
};