const mysql = require('mysql');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const MySQL = require('loopback-connector-mysql').MySQL;
const EnumFactory = require('loopback-connector-mysql').EnumFactory;
const fs = require('fs');

class VnMySQL extends MySQL {
    /**
     * Promisified version of execute().
     *
     * @param {String} query The SQL query string
     * @param {Array} params The query parameters
     * @param {Object} options The loopback options
     * @param {Function} cb The callback
     * @return {Promise} The operation promise
     */
    executeP(query, params, options = {}, cb) {
        return new Promise((resolve, reject) => {
            this.execute(query, params, options, (error, response) => {
                if (cb)
                    cb(error, response);
                if (error)
                    reject(error);
                else
                    resolve(response);
            });
        });
    }

    /**
     * Executes an SQL query from an Stmt.
     *
     * @param {ParameterizedSql} stmt - Stmt object
     * @param {Object} options Query options (Ex: {transaction})
     * @return {Object} Connector promise
     */
    executeStmt(stmt, options) {
        return this.executeP(stmt.sql, stmt.params, options);
    }

    /**
     * Executes a query from an SQL script.
     *
     * @param {String} sqlScript The sql script file
     * @param {Array} params The query parameters
     * @param {Object} options Query options (Ex: {transaction})
     * @return {Object} Connector promise
     */
    executeScript(sqlScript, params, options) {
        return new Promise((resolve, reject) => {
            fs.readFile(sqlScript, 'utf8', (err, contents) => {
                if (err) return reject(err);
                this.execute(contents, params, options)
                    .then(resolve, reject);
            });
        });
    }

    /**
     * Build the SQL WHERE clause for the where object without checking that
     * properties exists in the model.
     *
     * @param {object} where An object for the where conditions
     * @return {ParameterizedSQL} The SQL WHERE clause
     */
    makeWhere(where) {
        let wrappedConnector = Object.create(this);
        Object.assign(wrappedConnector, {
            getModelDefinition() {
                return {
                    properties: new Proxy({}, {
                        get: () => true
                    })
                };
            },
            toColumnValue(_, val) {
                return val;
            },
            columnEscaped(_, property) {
                return this.escapeName(property);
            }
        });

        return wrappedConnector.buildWhere(null, where);
    }

    /**
     * Constructs SQL order clause from Loopback filter.
     *
     * @param {Object} order The order definition
     * @return {String} Built SQL order
     */
    makeOrderBy(order) {
        if (!order)
            return '';
        if (typeof order === 'string')
            order = [order];

        let clauses = [];

        for (let clause of order) {
            let sqlOrder = '';
            let t = clause.split(/[\s,]+/);

            sqlOrder += this.escapeName(t[0]);

            if (t.length > 1)
                sqlOrder += ' ' + (t[1].toUpperCase() == 'ASC' ? 'ASC' : 'DESC');

            clauses.push(sqlOrder);
        }

        return `ORDER BY ${clauses.join(', ')}`;
    }

    /**
     * Constructs SQL limit clause from Loopback filter.
     *
     * @param {Object} filter The loopback filter
     * @return {String} Built SQL limit
     */
    makeLimit(filter) {
        let limit = parseInt(filter.limit);
        let offset = parseInt(filter.offset || filter.skip);
        return this._buildLimit(null, limit, offset);
    }

    /**
     * Constructs SQL pagination from Loopback filter.
     *
     * @param {Object} filter The loopback filter
     * @return {String} Built SQL pagination
     */
    makePagination(filter) {
        return ParameterizedSQL.join([
            this.makeOrderBy(filter.order),
            this.makeLimit(filter)
        ]);
    }

    /**
     * Constructs SQL filter including where, order and limit
     * clauses from Loopback filter.
     *
     * @param {Object} filter The loopback filter
     * @return {String} Built SQL filter
     */
    makeSuffix(filter) {
        return ParameterizedSQL.join([
            this.makeWhere(filter.where),
            this.makePagination(filter)
        ]);
    }

    /**
     * Constructs SQL where clause from Loopback filter discarding
     * properties that not pertain to the model. If defined, appends
     * the table alias to each field.
     *
     * @param {String} model The model name
     * @param {Object} where The loopback where filter
     * @param {String} tableAlias Query main table alias
     * @return {String} Built SQL where
     */
    buildModelWhere(model, where, tableAlias) {
        let parent = this;
        let wrappedConnector = Object.create(this);
        Object.assign(wrappedConnector, {
            columnEscaped(model, property) {
                let sql = tableAlias
                    ? this.escapeName(tableAlias) + '.'
                    : '';
                return sql + parent.columnEscaped(model, property);
            }
        });

        return wrappedConnector.buildWhere(model, where);
    }

    /**
     * Constructs SQL where clause from Loopback filter  discarding
     * properties that not pertain to the model. If defined, appends
     * the table alias to each field.
     *
     * @param {String} model The model name
     * @param {Object} filter The loopback filter
     * @param {String} tableAlias Query main table alias
     * @return {String} Built SQL suffix
     */
    buildModelSuffix(model, filter, tableAlias) {
        return ParameterizedSQL.join([
            this.buildModelWhere(model, filter.where, tableAlias),
            this.makePagination(filter)
        ]);
    }
}

exports.VnMySQL = VnMySQL;

exports.initialize = function initialize(dataSource, callback) {
    dataSource.driver = mysql;
    dataSource.connector = new VnMySQL(dataSource.settings);
    dataSource.connector.dataSource = dataSource;

    const modelBuilder = dataSource.modelBuilder;
    const defineType = modelBuilder.defineValueType ?
        modelBuilder.defineValueType.bind(modelBuilder) :
        modelBuilder.constructor.registerType.bind(modelBuilder.constructor);

    defineType(function Point() {});

    dataSource.EnumFactory = EnumFactory;

    if (callback) {
        if (dataSource.settings.lazyConnect) {
            process.nextTick(function() {
                callback();
            });
        } else
            dataSource.connector.connect(callback);
    }
};