import {IConditionExpression} from "./condition-expression.interface";
import {ISelectItem} from "./select-item.interface";
import {QueryKind} from "./query-type.enum";
import {isArray, isString, trim, endsWith, find, includes} from "lodash";
const isNumber = require("is-number");

export default class Sql {

    private _queryKind: QueryKind;
    private _selects: Array<ISelectItem>;
    private _tables: Array<string>;
    private _wheres: Array<string>;
    private _whereConcat: string;
    private _whereConcatDefault: string;
    private _sets: Array<any>;
    private _limit: number;
    private _offset: number;
    private _orderBys: Array<any>;
    private _joins: Array<any>;
    private _groupBys: Array<string>;
    private _whereGroupCount: number;
    private _openWhereGroupCount: number;
    private _havings: Array<any>;

    constructor() {
        this.reset();
    }

    private reset() {
        this._queryKind = undefined;
        this._selects = [];
        this._tables = [];
        this._wheres = [];
        this._whereConcat = "and";
        this._whereConcatDefault = "and";
        this._sets = [];
        this._limit = undefined;
        this._offset = undefined;
        this._orderBys = [];
        this._joins = [];
        this._groupBys = [];
        this._whereGroupCount = 0;
        this._openWhereGroupCount = 0;
        this._havings = [];
        return this;
    }

    select();
    select(field: string);
    select(func: string, field: any, alias: string);
    select(field?: any, alias?: string, func?: string) {
        this._queryKind = QueryKind.SELECT;
        switch (arguments.length) {
            case 0:
                this._selects.push({ "field": "*" });
                break;
            case 1:
                if (!isArray(field)) field = String(field).split(",");
                (field as Array<string>).map(f => trim(f)).filter(Boolean).forEach(f => {
                    var [field, alias] = f.split(/\s*as\s*/i);
                    this._selects.push({ field, alias });
                });
                break;
            case 2:
                if (isString(field)) {
                    this._selects.push({ field, alias });
                }
                break;
            default:
                var args: Array<any> = Array.prototype.slice.call(arguments);
                func = args.shift(); // First parameter
                alias = args.pop(); // Last parameter
                args = args.map(f => isArray(f) ? f.join(", ") : f); // Do flatten array.
                this._selects.push({ func, alias, "field": args.join(", ") });
        }
        return this;
    }

    between(field, value, otherValue) {
        if (otherValue === undefined) {
            var split = value.split("..");
            if (split.length > 1) {
                [value, otherValue] = split;
            }
        }
        var sql = field + " between " + this._wrap(value) + " and " + this._wrap(otherValue);
        this._where(sql);
        return this;
    }

    table(table: Array<string>);
    table(table: string);
    table(table: any) {
        if (!isArray(table)) {
            table = [table];
        }
        table.forEach(t => this._tables.push(t));
        return this;
    }

    from(table: Array<string>);
    from(table: string);
    from(table: any) {
        return this.table(table);
    }

    into(table: string);
    into(table: Array<string>);
    into(table: any) {
        return this.table(table);
    }

    private static _operators = [">=", "<=", "!=", "<>", ">", "<", "!@", "@", "%$", "^%", "%", "like", "not like", "is", "is null", "is not null"];

    private _conditionExpr(field: string, value?: any, operator?: string): IConditionExpression {
        // console.log('_conditionExpr', arguments);
        var wrapValue = true;
        if (!operator) {
            operator = find(Sql._operators, o => endsWith(field, o));
            if (operator) {
                field = field.slice(0, -operator.length).trim();
            }
        }
        switch (operator) {
            case "is null":
                operator = "is";
                value = "@null";
                break;
            case "is not null":
                operator = "is not";
                value = "@null";
                break;
            default:
                if (!operator) {
                    operator = "=";
                }
        }
        if (value === null) {
            value = "@null";
        }
        if (isString(value) && value.substr(0, 1) === "@") {
            wrapValue = false;
            value = value.substr(1);
        }
        if (wrapValue && value !== undefined) {
            value = this._wrap(value);
        }
        return { field, operator, value };
    }

    where(field, operator?, value?) {
        if (typeof field === "object") {
            for (var i in field) {
                this.where(i, field[i]);
            }
            return this;
        }
        if (value === undefined) {
            value = operator;
            operator = undefined;
        }
        // console.log("field, value, operator ", field, value, operator);
        var expr = this._conditionExpr(field, value, operator);
        // console.log("expr ", expr);
        switch (expr.operator) {
            case "@": return this.whereIn(field.slice(0, -1), value);
            case "!@": return this.whereNotIn(field.slice(0, -2), value);
            case "!%": return this.notLike(field.slice(0, -2), value, "both");
            case "%": return this.like(field.slice(0, -1), value, "both");
            case "^%": return this.like(field.slice(0, -2), value, "right");
            case "%$": return this.like(field.slice(0, -2), value, "left");
        }
        this._where(`${expr.field} ${expr.operator} ${expr.value}`);
        return this;
    }

    whereNotIn(field, values) {
        return this.whereIn(field, values, "not in");
    }

    whereIn(field, values: Array<string>, op = "in") {
        if (!isArray(values)) values = [String(values)];
        values = values.map(this._wrap);
        var value = "(" + values.join(",") + ")";
        var where = field + " " + op + " " + value;
        this._where(where);
        return this;
    }

    limit(limit: number, offset?: number) {
        this._limit = limit;
        if (offset !== undefined) {
            this._offset = offset;
        }
        return this;
    }

    top(n: number) {
        return this.limit(n);
    }

    offset(offset: number) {
        this._offset = offset;
        return this;
    }

    skip(n: number) {
        return this.offset(n);
    }

    notLike(field, match, side) {
        return this.like(field, match, side, "not like");
    }

    like(field: string, match = "", side = "both", op = "like") {
        field = trim(field);
        switch (side) {
            case "left": match = "%" + match; break;
            case "right": match += "%"; break;
            case "both": {
                if (isString(match) && match.length === 0) {
                    match = "%";
                } else {
                    match = "%" + match + "%";
                }
            } break;
            default:
                throw new Error(`Unknown side ${side}`);
        }
        // console.log('field', JSON.stringify(field));
        // console.log('op', JSON.stringify(op));
        // console.log('match', JSON.stringify(match));
        // console.log(JSON.stringify(`${field} ${op} ${this._wrap(match)}`));
        this._where(`${field} ${op} ${this._wrap(match)}`);
        return this;
    }

    groupBy(fields: Array<string>) {
        if (!isArray(fields)) {
            fields = String(fields).split(",");
        }
        fields
            .map(f => trim(f))
            .filter(f => f.length > 0)
            .forEach(f => {
                this._groupBys.push(f);
            });
        return this;
    }

    andOp() {
        this._whereConcat = "and";
        return this;
    }

    orOp() {
        this._whereConcat = "or";
        return this;
    }

    beginWhereGroup() {
        this._whereGroupCount++;
        this._openWhereGroupCount++;
        return this;
    }

    endWhereGroup() {
        if (this._whereGroupCount > 0) {
            var whereCount = this._wheres.length;
            if (this._openWhereGroupCount >= this._whereGroupCount) {
                this._openWhereGroupCount--;
            } else if (whereCount > 0) {
                this._wheres[whereCount - 1] += ")";
            }
            this._whereGroupCount--;
        }
        return this;
    }

    private _endQuery() {
        while (this._whereGroupCount > 0) {
            this.endWhereGroup();
        }
    }

    private static _joinTypes = ["inner", "outer", "left", "right", "left outer", "right outer"];

    join(table: string, on: string, join: string) {
        if (!includes(Sql._joinTypes, join)) join = "";
        var expr = trim(`${join} join ${table} on ${on}`);
        this._joins.push(expr);
        return this;
    }

    leftJoin(table: string, on: string) {
        return this.join(table, on, "left");
    }

    having(field, value) {
        var expr = this._conditionExpr(field, value);
        this._havings.push(`${expr.field} ${expr.operator} ${expr.value}`);
        return this;
    }

    orderBy(field, direction: string = "asc") {
        direction = direction.toLowerCase();
        if (direction !== "asc") direction = "desc";
        var expr = `${field} ${direction}`;
        this._orderBys.push(expr);
        return this;
    }

    get() {
        switch (this._queryKind) {
            case QueryKind.SELECT: return this.getSelect();
            case QueryKind.DELETE: return this.getDelete();
            case QueryKind.INSERT: return this.getInsert();
            case QueryKind.UPDATE: return this.getUpdate();
            default:
                throw new TypeError(`Unknown kind of query ${this._queryKind}`);
        }
    }

    getSelect() {
        this._endQuery();
        var sql = "select ";
        var selects = this._selects
            .map(item => {
                var field = item.field;
                if (item.func) field = item.func + "(" + field + ")";
                if (item.alias) field = field + " as " + item.alias;
                return field;
            })
            .join(", ");
        if (selects === "") selects = "*";
        sql += selects;
        if (this._tables.length > 0) sql += " " + "from " + this._tables.join(", ");
        if (this._joins.length > 0) sql += " " + this._joins.join(" ");
        if (this._wheres.length > 0) sql += " " + "where " + this._wheres.join(" ");
        if (this._groupBys.length > 0) sql += " " + "group by " + this._groupBys.join(", ");
        if (this._havings.length > 0) sql += " " + "having " + this._havings.join(" ");
        if (this._orderBys.length > 0) sql += " " + "order by " + this._orderBys.join(", ");
        if (isNumber(this._limit)) {
            sql += " ";
            sql = this.getLimit(sql, this._limit, this._offset);
        }
        this.reset();
        return sql;
    }

    delete(table?: string) {
        this._queryKind = QueryKind.DELETE;
        if (table) {
            this._tables.push(table);
        }
        return this;
    }

    getDelete() {
        var table = this._tables[0];
        var result = "delete " + table;
        if (this._wheres.length > 0) {
            result += " " + "where " + this._wheres.join(" ");
        }
        this.reset();
        return result;
    }

    update(table?: string) {
        this._queryKind = QueryKind.UPDATE;
        if (table) {
            this._tables.push(table);
        }
        return this;
    }

    getUpdate() {
        this._endQuery();
        var table = this._tables[0];
        var result = "update " + table + " set ";
        result += this._sets
            .map(item => {
                var value = item.value;
                if (item.wrapValue) value = this._wrap(value);
                return `${item.name} = ${value}`;
            })
            .join(", ");
        if (this._wheres.length > 0) {
            result += " where " + this._wheres.join(" ");
        }
        this.reset();
        return result;
    }

    insert() {
        this._queryKind = QueryKind.INSERT;
        return this;
    }

    set(name, value, wrapValue: boolean = true) {
        if (arguments.length === 1) {
            for (var i in name) {
                this.set(i, name[i], true);
            }
            return this;
        }
        if (value === null || value === undefined) {
            value = "null";
            wrapValue = false;
        }
        this._sets.push({
            name: name,
            value: value,
            wrapValue: wrapValue
        });
        return this;
    }

    getInsert() {
        var table = this._tables[0];
        var result = "insert into " + table;
        var names = this._sets.map(item => item.name);
        var values = this._sets.map(item => {
            var value = item.value;
            if (item.wrapValue) value = this._wrap(value);
            return value;
        });
        result += "(" + names.join(", ") + ") values(" + values.join(", ") + ")";
        this.reset();
        return result;
    }

    getLimit(sql, limit, offset): string {
        throw "getLimit() not supported.";
    }

    private _where(sql: string) {
        var concat = "";
        if (this._wheres.length > 0) {
            concat = this._whereConcat + " ";
        }
        while (this._openWhereGroupCount > 0) {
            concat += "(";
            this._openWhereGroupCount--;
        }
        // console.log("concat _w ", JSON.stringify(concat));
        // console.log("sql _w ", JSON.stringify(sql));
        this._whereConcat = this._whereConcatDefault;
        this._wheres.push(concat + sql);
        // console.log("this._wheres ", this._wheres);
    }

    private _wrap(value: any) {
        // console.log("_wrap ", value);
        if (!isNumber(value)) {
            value = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
                .replace(/'/g, "\\'")
                .replace(/\\"/g, '"') + '\'';
        }
        return value;
    }

    private _quote(value: string, wrapInQuotes: boolean) {
        console.log("_quote ", value);
        if (isNumber(value)) {
            return value;
        }
        value = value
            .replace("\\", "\\\\")
            .replace("\0", "\\0")
            .replace("\n", "\\n")
            .replace("\r", "\\r")
            .replace("'", "\\'")
            .replace("\"", "\\\"")
            .replace("\x1a", "\\Z");
        if (wrapInQuotes) {
            value = "'" + value + "'";
        }
        return value;
    }
}
