const { throwTypeError, throwRangeError } = require("./Errors");


/**
 * Prepares a new alteration of a table
 */
class AlterTable {
    #instance;
    #database;
    #name;
    #structure;
    constructor(instance, defaultDatabase, name){
        this.#instance = instance;
        this.#database = defaultDatabase;
        this.#name = name;
    }

    /**
     * Selects a database for this query
     * @param {String} database - Name of the database 
     * @returns {this}
     */
    selectDatabase(database){
        if (!database) throw new Error("database must not be empty");
        if (typeof database !== "string") throwTypeError("string", database);
        this.#database = database;
        return this;
    }

    /**
     * The new desired structure.
     * - Drops columns that are existing in the current table but not in this structure
     * - Adds columns that are missing in the current table
     * - Modifies all other columns where at least one datatype is not matching
     * @param {Structure} struc - New structure for the table 
     * @returns {this}
     */
    structure(struc){
        if (!struc) throw new Error("struc must not be empty");
        if (typeof struc !== "object") throwTypeError("object", struc);
        this.#structure = struc;
        return this;
    }

    /**
     * Executes the prepared querry
     * @returns {Any}
     */
    async execute(){
        if (!this.#database) throw new Error(`Can't alter table ${this.#name}: Database not selected`);
        if (!this.#name) throw new Error(`Can't alter table on ${this.#database}: No name given`);
        if (!this.#instance.isConnected()) throw new Error(`Can't execute query: Instance has no connection`);
        if (!this.#structure) throw new Error(`Can't alter table ${this.#database}.${this.#name}: No new structure given`);
        const currentStruc = (await this.#instance.getStructure(this.#name, this.#database)).get();
        const newStruc = this.#structure.get();
        if (currentStruc.length===0) throw new Error(`Can't alter table ${this.#name}: Table does not exist`);
        for (let col of currentStruc){
            if (!newStruc.find(newCol => newCol.Field === col.Field)){
                // DELETE COLUMN
                await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP COLUMN \`${col.Field}\``);
            }
        }
        for (let col of newStruc){
            const oldStrucCol = currentStruc.find((oldCol) => oldCol.Field === col.Field);
            if (!oldStrucCol){
                // ADD COLUMN
                await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD COLUMN \`${col.Field}\` ${col.Type}${col.Extra?` ${col.Extra}`:""}${col.Null==="YES"?" NULL":" NOT NULL"}${col.Default!==null?` DEFAULT '${col.Default}'`:""}`);
                if (col.Key){
                    switch(col.Key){
                        case "PRI":
                            await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD PRIMARY KEY (${col.Field});`);
                            break;
                        case "MUL":
                            await this.#instance.queryRaw(`CREATE INDEX ${col.Field} ON ${this.#database}.${this.#name} (${col.Field})`);
                            break;
                        case "UNI":
                            await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD UNIQUE (${col.Field});`);
                            break;
                    }
                }
                continue;
            }
            for (let key in col){
                if (oldStrucCol[key] !== col[key]){
                    // UPDATE COLUMN
                    await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} MODIFY COLUMN \`${col.Field}\` ${col.Type}${col.Extra?` ${col.Extra}`:""}${col.Null==="YES"?" NULL":" NOT NULL"}${col.Default!==null?` DEFAULT '${col.Default}'`:""}`);
                    if (oldStrucCol.Key){
                        switch(oldStrucCol.Key){
                            case "PRI":
                                await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP PRIMARY KEY;`);
                                break;
                            case "MUL":
                                await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP INDEX ${col.Field};`);
                                break;
                            case "UNI":
                                await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP UNIQUE (${col.Field});`);
                                break;
                        }
                    }
                    if (col.Key){
                        switch(col.Key){
                            case "PRI":
                                await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD PRIMARY KEY (${col.Field});`);
                                break;
                            case "MUL":
                                await this.#instance.queryRaw(`CREATE INDEX ${col.Field} ON ${this.#database}.${this.#name} (${col.Field})`);
                                break;
                            case "UNI":
                                await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD UNIQUE (${col.Field});`);
                                break;
                        }
                    }
                    break;
                }
            }
        }
        return await this.#instance.checkStructure(this.#name, newStruc, this.#database);
    }
}

/**
 * Prepares creation of a new table
 */
class CreateTable {
    #instance;
    #database;
    #name;
    #structure;
    constructor(instance, defaultDatabase, name) {
        this.#instance = instance;
        this.#database = defaultDatabase;
        this.#name = name;
    }

    /**
     * Selects a database for this query
     * @param {String} database - Name of the database 
     * @returns {this}
     */
    selectDatabase(database){
        if (!database) throw new Error("database must not be empty");
        if (typeof database !== "string") throwTypeError("string", database);
        this.#database = database;
        return this;
    }

    /**
     * Sets a new name
     * @param {String} name 
     * @returns {this}
     */
    name(name){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        this.#name = name;
        return this;
    }

    /**
     * Defines the structure for the table
     * @param {Structure} struc - Instance of Structure
     * @returns {this} 
     */
    structure(struc){
        if (!struc) throw new Error("struc must not be empty");
        if (typeof struc !== "object") throwTypeError("object", struc);
        this.#structure = struc;
        return this;
    }

    /**
     * Executes the prepared querry
     * @returns {Any}
     */
    async execute(){
        if (!this.#database) throw new Error(`Can't create table ${this.#name}: Database not selected`);
        if (!this.#name) throw new Error(`Can't create table on ${this.#database}: No name given`);
        if (!this.#instance.isConnected()) throw new Error(`Can't execute query: Instance has no connection`);
        if (!this.#structure) throw new Error(`Can't create table ${this.#database}.${this.#name}: No structure given`);
        const colObjs = this.#structure.get();

        const coltypes = colObjs.map(obj => `\`${obj.Field}\` ${obj.Type}${obj.Null==="YES"?" NULL":" NOT NULL"}${obj.Default?` DEFAULT '${obj.Default}'`:""}${obj.Extra?` ${obj.Extra}`:""}`);
        const constraints = colObjs.map(obj => `${obj.Key?obj.Key==="PRI"?` PRIMARY KEY (${obj.Field})`:obj.Key==="MUL"?` INDEX (${obj.Field})`:obj.Key==="UNI"?` UNIQUE (${obj.Key})`:"":""}`)
            .filter(str => str !== ""); // Filter out empty

        const queryString = `CREATE TABLE ${this.#database}.${this.#name} (${coltypes}${constraints.length>0?",":""}${constraints})`;
        return await this.#instance.queryRaw(queryString);
    }

}

/**
 * @typedef {Object} ConstraintOptions
 * @property {Boolean} [primary] - Whether this column should be primary
 * @property {Boolean} [index] - Whether this column should be indexable (Faster query, slower insertion)
 * @property {Boolean} [null] - Whether this column is null per default
 * @property {Boolean} [unique] - Whether this column data should be unique
 * @property {String} [default] - Set's the default data for this column
 * @property {Boolean} [auto_increment] - Whether this column should be numerical auto_increment
 * @property {Boolean} [unsigned] - Only on numerical: Whether this numerical field should be unsigned
 */

/**
 * Creates a new table-structure
 */
class Structure {
    #columns = [];
    constructor(tableDescription) {
        if (!tableDescription) return;
        this.#columns = tableDescription;
    }

    /**
     * Drops (Removes) a column name
     * @param {String} name - The name of the column 
     * @returns {this}
     */
    drop(name){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        let index;
        for (let i = 0; i < this.#columns.length; i++){
            if (this.#columns[i].Field === name) {
                index=i;
                break;
            }
        }
        this.#columns.splice(index, 1);
        return this;
    }

    /**
     * Returns the columns 
     * @returns {Array<ColumnStructure>}
     */
    get(){
        return this.#columns;
    }

    /**
     * Adds a new column of data type 'char' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Length of characters. Min 0, Max 255. Default 1
     * @param {ConstraintOptions} [options] - Extra constraint options
     * @returns {this}
     */
    char(name, size=1, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 0 || size > 255){
            throwRangeError(0, 255, size);
        }
        this.#columns.push(parseColumnData(name, `char(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'varchar' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Length of characters. Min 0, Max 255. Default 1 
     * @param {ConstraintOptions} [options] - Extra constraint options
     * @returns {this}
     */
    varchar(name, size=8, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 0 || size > 255){
            throwRangeError(0, 255, size);
        }
        this.#columns.push(parseColumnData(name, `varchar(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'binary' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Length of data. Min 1 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    binary(name, size=1, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1){
            throwRangeError(1, "*", size);
        }
        this.#columns.push(parseColumnData(name, `binary(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'varbinary' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Length of data. Min 1 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    varbinary(name, size=1, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1){
            throwRangeError(1, "*", size);
        }

        this.#columns.push(parseColumnData(name, `varbinary(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'tinyblob' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    tinyblob(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `tinyblob`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'tinytext' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    tinytext(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `tinytext`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'text' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options
     * @returns {this}
     */
    text(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `text`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'blob' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Size in bytes. Min 1, Max 65535. Defaults to 65535 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    blob(name, size=65535, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 65535) throwRangeError(1, 65535, size);
        this.#columns.push(parseColumnData(name, `blob(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'mediumtext' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    mediumtext(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `mediumtext`, options));
        return this;
    } 

    /**
     * Adds a new column of data type 'longtext' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options
     * @returns {this}
     */
    longtext(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `longtext`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'longblob' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    longblob(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `longblob`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'enum' to this structure
     * @param {String} name - Name of the column 
     * @param {Array<String>} vals - Array of possible values 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    enum(name, vals=[], options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (!Array.isArray(vals)) throwTypeError("array", vals);
        if (vals.length===0) throw new Error("vals must not be empty");
        if (typeof options !== "object") throwTypeError("object", options);
        if (!Array.isArray(vals)) throw new Error(`Column datatype 'enum': 'vals' must be of type Array`);
        if (vals.length<=0) throw new Error(`Column datatype 'enum' must contain a list of possible values. Received undefined`);
        this.#columns.push(parseColumnData(name, `enum(${vals.map(val=>`'${val}'`)})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'enum' to this structure
     * @param {String} name - Name of the column 
     * @param {Array<String>} vals - Array of possible values 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    set(name, vals=[], options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (!Array.isArray(vals)) throwTypeError("array", vals);
        if (vals.length===0) throw new Error("vals must not be empty");
        if (typeof options !== "object") throwTypeError("object", options);
        if (!Array.isArray(vals)) throw new Error(`Column datatype 'set': 'vals' must be of type Array`);
        if (vals.length<=0) throw new Error(`Column datatype 'set' must contain a list of possible values. Received undefined`);
        this.#columns.push(parseColumnData(name, `set(${vals.map(val=>`'${val}'`)})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'bit' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Min 1, Max 64. Default to 1 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    bit(name, size=1, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 64) throwRangeError(1, 64, size);
        this.#columns.push(parseColumnData(name, `bit(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'tinyint' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Min 1, Max 255. Defaults to 255 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    tinyint(name, size=255, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 255) throwRangeError(1, 255, size);

        this.#columns.push(parseColumnData(name, `tinyint(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'bool' to this structure
     * @param {String} name - Name of the column
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    bool(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `tinyint(1)`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'smallint' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] Min 1, Max 255. Defaults to 255 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    smallint(name, size=255, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 255) throwRangeError(1, 255, size);
        this.#columns.push(parseColumnData(name, `smallint(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'mediumint' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] Min 1, Max 255. Defaults to 255 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    mediumint(name, size=255, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 255) throwRangeError(1, 255, size);
        this.#columns.push(parseColumnData(name, `mediumint(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'int' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] Min 1, Max 255. Defaults to 255 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    int(name, size=255, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 255) throwRangeError(1, 255, size);
        this.#columns.push(parseColumnData(name, `int(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'bigint' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] Min 1, Max 255. Defaults to 255 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    bigint(name, size=255, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 255) throwRangeError(1, 255, size);
        this.#columns.push(parseColumnData(name, `bigint(${size})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'float' to this structure
     * @param {String} name - Name of the column
     * @param {Number} [p] - Precision. Min 1, Max 53. Defaults to 25
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    float(name, p=25, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof p !== "number") throwTypeError("number", p);
        if (typeof options !== "object") throwTypeError("object", options);
        if (p < 1 || p > 53) throwRangeError(1, 53, p);

        this.#columns.push(parseColumnData(name, `float(${p})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'double' to this structure
     * @param {String} name - Name of the column 
     * @param {Number} [size] - Min 1. Defaults to 16 
     * @param {Number} [d] - Double precision. Min 1. Defaults to 8 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    double(name, size=16, d=8, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof d !== "number") throwTypeError("number", d);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1) throwRangeError(1, "*", size);
        if (d < 1) throwRangeError(1, "*", d);
        this.#columns.push(parseColumnData(name, `double(${size},${d})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'decimal' to this structure
     * @param {String} name - Name of the column
     * @param {Number} [size] - Min 1, Max 65. Defaults to 10
     * @param {Number} [d] - Double precision. Min 0. Defaults to 0. 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    decimal(name, size=10, d=0, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof size !== "number") throwTypeError("number", size);
        if (typeof d !== "number") throwTypeError("number", d);
        if (typeof options !== "object") throwTypeError("object", options);
        if (size < 1 || size > 65) throwRangeError(1, 65, size);
        if (d < 0) throwRangeError(0, "*", d);

        this.#columns.push(parseColumnData(name, `decimal(${size},${d})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'date' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    date(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `date`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'datetime' to this structure
     * @param {String} name - Name of the column
     * @param {Number} [fsp] - Fractional second precision. Min 0, Max 6. Defaults to 0
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    datetime(name, fsp=0, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof fsp !== "number") throwTypeError("number", fsp);
        if (typeof options !== "object") throwTypeError("object", options);
        if (fsp < 0 || fsp > 6) throwRangeError(0, 6, fsp);

        this.#columns.push(parseColumnData(name, `datetime(${fsp})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'timestamp' to this structure
     * @param {String} name - Name of the column
     * @param {Number} [fsp] - Fractional second precision. Min 0, Max 6. Defaults to 0
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    timestamp(name, fsp=0, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof fsp !== "number") throwTypeError("number", fsp);
        if (typeof options !== "object") throwTypeError("object", options);
        if (fsp < 0 || fsp > 6) throwRangeError(0, 6, fsp);

        this.#columns.push(parseColumnData(name, `timestamp(${fsp})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'time' to this structure
     * @param {String} name - Name of the column
     * @param {Number} [fsp] - Fractional second precision. Min 0, Max 6. Defaults to 0
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    time(name, fsp=0, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof fsp !== "number") throwTypeError("number", fsp);
        if (typeof options !== "object") throwTypeError("object", options);
        if (fsp < 0 || fsp > 6) throwRangeError(0, 6, fsp);

        this.#columns.push(parseColumnData(name, `time(${fsp})`, options));
        return this;
    }

    /**
     * Adds a new column of data type 'year' to this structure
     * @param {String} name - Name of the column 
     * @param {ConstraintOptions} [options] - Extra constraint options 
     * @returns {this}
     */
    year(name, options={}){
        if (!name) throw new Error("name must not be empty");
        if (typeof name !== "string") throwTypeError("string", name);
        if (typeof options !== "object") throwTypeError("object", options);
        this.#columns.push(parseColumnData(name, `time`, options));
        return this;
    }
}

/**
 * Parses the given column data
 * @private
 * @param {String} name - Column name 
 * @param {String} type - Column type 
 * @param {ConstraintOptions} options 
 * @returns {ColumnStructure}
 */
function parseColumnData(name, type, options={}){
    if (options.primary + options.unique + options.index > 1) throw new Error(`ConstrainError: Only one of 'primary', 'unique' or 'index' allowed`); 
    return {
        Field: name,
        Type: `${type}${options.unsigned?" unsigned":""}`,
        Null: options.null?"YES":"NO",
        Key: options.primary?"PRI":options.index?"MUL":options.unique?"UNI":"",
        Default: options.default!==undefined?options.default.toString():null,
        Extra: options.auto_increment?"auto_increment":""
    }
}

/**
 * @typedef {Object} ColumnStructure
 * @property {String} Field - Column name
 * @property {String} Type - Column type
 * @property {"YES"|"NO"} Null - Whether the column allows null-values
 * @property {""|"PRI"|"MUL"|"UNI"} Key - PRI = Primary, MUL = Indexed, UNI = Unique
 * @property {String|null} Default - Default values for this row
 * @property {""|"auto_increment"} Extra - Any extra
 */


module.exports = { CreateTable, Structure, AlterTable }