'use strict';

const mysql = require('mysql');
const dataHelp = require('./util/data.helper.js');
const whereHelp = require('./util/whereClause.helper.js');
const assert = require('assert')


//define class§
class Xsql {

  constructor(sqlConfig, pool) {

    //define this variables
    this.sqlConfig = {}
    this.pool = {}
    this.metaDb = {};
    this.metaDb.tables = {};

    this.sqlConfig = sqlConfig;
    this.pool = pool;

  }

  init(cbk) {
    this.dbCacheInitAsync((err, results) => {
      cbk(err, results)
    })
  }


  dbCacheInitAsync(cbk) {

    let self = this;

    self.pool.query(dataHelp.getSchemaQuery(), [this.sqlConfig.database], (err, results) => {

      if (err) {
        console.log('Cache init failed during database reading')
        console.log(err, results)
        cbk(err, results)
      } else {

        self.iterateToCacheTables(results)
        self.iterateToCacheTablePks(results)
        self.iterateToCacheTableColumns(results)
        self.iterateToCacheTableFks(results)

        // osx mysql server has limitations related to open_tables
        self.pool.query('FLUSH TABLES', [], (err, results) => {
          cbk(null, null)
        })
      }
    })

  }


  iterateToCacheTables(schemaResults) {

    for (let i = 0; i < schemaResults.length; ++i) {

      let schemaRow = schemaResults[i];

      let tableName = schemaRow['table_name'];

      if (!(tableName in this.metaDb.tables)) {
        this.metaDb.tables[tableName] = {}
        this.metaDb.tables[tableName]['primaryKeys'] = []
        this.metaDb.tables[tableName]['foreignKeys'] = []
        this.metaDb.tables[tableName]['columns'] = []
        this.metaDb.tables[tableName]['indicies'] = []
      }
    }
  }

  iterateToCacheTableColumns(schemaResults) {

    for (let i = 0; i < schemaResults.length; ++i) {
      let schemaRow = schemaResults[i];
      let tableName = schemaRow['table_name'];
      let col = {};
      col['column_name'] = schemaRow['column_name']
      col['ordinal_position'] = schemaRow['ordinal_position']
      col['column_key'] = schemaRow['column_key']
      col['data_type'] = schemaRow['data_type']
      col['column_type'] = schemaRow['column_type']

      dataHelp.findOrInsertObjectArrayByKey(col, 'column_name', this.metaDb.tables[tableName]['columns'])

    }
  }

  iterateToCacheTablePks(schemaResults) {

    for (let i = 0; i < schemaResults.length; ++i) {
      let schemaRow = schemaResults[i];
      let tableName = schemaRow['table_name'];

      if (schemaRow['column_key'] === 'PRI') {

        let pk = {};
        pk['column_name'] = schemaRow['column_name']
        pk['ordinal_position'] = schemaRow['ordinal_position']
        pk['column_key'] = schemaRow['column_key']
        pk['data_type'] = schemaRow['data_type']
        pk['column_type'] = schemaRow['column_type']

        dataHelp.findOrInsertObjectArrayByKey(pk, 'column_name', this.metaDb.tables[tableName]['primaryKeys'])

      }
    }
  }

  iterateToCacheTableFks(schemaResults) {

    for (let i = 0; i < schemaResults.length; ++i) {

      let schemaRow = schemaResults[i];
      let tableName = schemaRow['table_name'];

      if (schemaRow['referenced_table_name']) {

        let fk = {};

        fk['column_name'] = schemaRow['column_name']
        fk['table_name'] = schemaRow['table_name']
        fk['referenced_table_name'] = schemaRow['referenced_table_name']
        fk['referenced_column_name'] = schemaRow['referenced_column_name']
        fk['data_type'] = schemaRow['data_type']
        fk['column_type'] = schemaRow['column_type']

        dataHelp.findOrInsertObjectArrayByKey(fk, 'column_name', this.metaDb.tables[tableName]['foreignKeys'])

        //console.log(fk['referenced_table_name'],fk['referenced_column_name'],tableName, schemaRow['column_name'], this.metaDb.tables[tableName]['foreignKeys'].length)
      }
    }
  }

  exec(query, params) {

    let _this = this;
    return new Promise(function (resolve, reject) {
      //console.log('mysql>', query, params);
      _this.pool.query(query, params, function (error, rows, _fields) {
        if (error) {
          console.log('mysql> ', error);
          return reject(error);
        }
        return resolve(rows);
      });
    });

  }

  getLimitClause(reqParams) {

    //defaults
    reqParams._index = 0;
    reqParams._len = 20;

    if ('_size' in reqParams && parseInt(reqParams._size) < 100) {
      reqParams._len = parseInt(reqParams._size)
    }

    if ('_p' in reqParams && parseInt(reqParams._p) > 0) {
      reqParams._index = (parseInt(reqParams._p) - 1) * reqParams._len + 1;
    }

    //console.log(reqParams._index, reqParams._len);

    return [reqParams._index, reqParams._len]

  }

  getGroupByClause(_groupby, tableName, queryParamsObj) {

    if (_groupby) {
      queryParamsObj.query += ' group by ' + _groupby + ' '
      return _groupby
    }

  }

  getHavingClause(_having, tableName, queryParamsObj) {

    if (_having) {

      let whereClauseObj = whereHelp.getConditionClause(_having, 'having')

      if (whereClauseObj.err === 0) {
        queryParamsObj.query = queryParamsObj.query + ' having ' + whereClauseObj.query;
        queryParamsObj.params = queryParamsObj.params.concat(whereClauseObj.params)
      }

      //console.log('> > > after where clause filling up:', queryParamsObj.query, queryParamsObj.params);
    }

  }

  getWhereClause(queryparams, tableName, queryParamsObj, appendToWhere) {

    if (queryparams) {

      let whereClauseObj = whereHelp.getConditionClause(queryparams)

      if (whereClauseObj.err === 0) {
        queryParamsObj.query = queryParamsObj.query + appendToWhere + whereClauseObj.query;
        queryParamsObj.params = queryParamsObj.params.concat(whereClauseObj.params)
      }

      //console.log('> > > after where clause filling up:', queryParamsObj.query, queryParamsObj.params);
    }

  }


  getOrderByClause(queryparams, tableName, queryParamsObj) {

    if (queryparams._sort) {

      queryParamsObj.query += ' ORDER BY '

      let orderByCols = queryparams._sort.split(',')

      for (let i = 0; i < orderByCols.length; ++i) {
        if (i) {
          queryParamsObj.query += ', '
        }
        if (orderByCols[i][0] === '-') {
          let len = orderByCols[i].length;
          queryParamsObj.query += ' ?? DESC'
          queryParamsObj.params.push(orderByCols[i].substring(1, len) )
        } else {
          queryParamsObj.query += ' ?? ASC'
          queryParamsObj.params.push(orderByCols[i])
        }
      }
    }

  }

  getColumnsForSelectStmtWithGrpBy(reqQueryParams, tableName, queryParamsObj) {

    let grpByCols = reqQueryParams._groupby.split(',');

    for (var i = 0; i < grpByCols.length; ++i) {
      if (i) {
        queryParamsObj.query += ','
      }
      queryParamsObj.query += ' ??'
      queryParamsObj.params.push(grpByCols[i])
    }

    queryParamsObj.query += ',count(1) as _count '

  }

  getColumnsForSelectStmt(tableName, reqQueryParams, queryParamsObj) {

    let table = this.metaDb.tables[tableName];
    let cols = [];
    let _fieldsInQuery = [];
    let removeFieldsObj = {};

    // populate _fields array from query params
    if ('_fields' in reqQueryParams) {
      _fieldsInQuery = reqQueryParams['_fields'].split(',')
    } else {
      queryParamsObj.query += ' * '
      return " * ";
    }


    // get column name in _fields and mark column name which start with '-'
    for (let i = 0; i < _fieldsInQuery.length; ++i) {
      if (_fieldsInQuery[i][0] === '-') {
        removeFieldsObj[_fieldsInQuery[i].substring(1, _fieldsInQuery[i].length)] = 1;
      } else {
        cols.push(_fieldsInQuery[i])
      }
    }

    if (!cols.length) {
      // for each column in table - add only which are not in removeFieldsObj
      for (let i = 0; i < table['columns'].length; ++i) {
        if (!(table['columns'][i]['column_name'] in removeFieldsObj)) {
          cols.push(table['columns'][i]['column_name'])
        }
      }
    } else {

      cols = this.removeUnknownColumns(cols, tableName)

    }

    for (var i = 0; i < cols.length; ++i) {
      if (i) {
        queryParamsObj.query += ','
      }
      queryParamsObj.query += '??'
      queryParamsObj.params.push(cols[i])
    }

    return cols.join(',')

  }


  removeUnknownColumns(inputColumns, tableName) {

    let cols = inputColumns;
    let unknown_cols_in_input = []
    let shadowCols = [];
    let tableColumns = this.metaDb.tables[tableName]['columns']

    // find unknown fields if any
    for (var j = 0; j < cols.length; ++j) {

      let found = 0;

      for (var i = 0; i < tableColumns.length; ++i) {
        if (tableColumns[i]['column_name'] === cols[j]) {
          found = 1;
          break;
        }
      }

      if (!found) {
        unknown_cols_in_input.push(j)
      }

    }

    // if there are unknown fields - remove and ignore 'em
    if (unknown_cols_in_input.length) {

      for (var i = 0; i < cols.length; ++i) {
        if (unknown_cols_in_input.indexOf(i) === -1) {
          shadowCols.push(cols[i])
        }
      }

      cols = [];
      cols = shadowCols;

    }

    return cols;

  }

  getPrimaryKeyWhereClause(tableName, pksValues) {

    let whereClause = '';
    let whereCol = '';
    let whereValue = '';
    let pks = []

    if (tableName in this.metaDb.tables) {
      pks = this.metaDb.tables[tableName].primaryKeys;
    } else {
      return null
    }

    // number of primary keys in table and one sent should be same
    if (pksValues.length !== pks.length) {
      return null
    }

    // get a where clause out of the above columnNames and their values
    for (let i = 0; i < pks.length; ++i) {

      let type = getColumnType(pks[i]);

      whereCol = pks[i]['column_name']

      if (type === 'string') {
        whereValue = mysql.escape(pksValues[i])
      } else if (type === 'int') {
        whereValue = parseInt(pksValues[i])
      } else if (type === 'float') {
        whereValue = parseFloat(pksValues[i])
      } else if (type === 'date') {
        whereValue = Date(pksValues[i])
      } else {
        console.error(pks[i])
        assert(false, 'Unhandled type of primary key')
      }

      if (i) {
        whereClause += ' and '
      }

      whereClause += whereCol + ' = ' + whereValue;

    }

    return whereClause;

  }

  getForeignKeyWhereClause(parentTable, parentId, childTable) {

    let whereValue = '';

    //get all foreign keys of child table
    let fks = this.metaDb.tables[childTable].foreignKeys;
    let fk = dataHelp.findObjectInArrayByKey('referenced_table_name', parentTable, fks);
    let whereCol = fk['column_name']
    let colType = getColumnType(fk);

    if (colType === 'string') {
      whereValue = mysql.escape(parentId)
    } else if (colType === 'int') {
      whereValue = mysql.escape(parseInt(parentId))
    } else if (colType === 'float') {
      whereValue = mysql.escape(parseFloat(parentId))
    } else if (colType === 'date') {
      whereValue = mysql.escape(Date(parentId))
    } else {
      console.error(pks[i])
      assert(false, 'Unhandled column type in foreign key handling')
    }

    return whereCol + ' = ' + whereValue;

  }

  prepareRoute(internal, httpType, apiPrefix, urlRoute, routeType) {

    let route = {};
    route['httpType'] = httpType;
    route['routeUrl'] = apiPrefix + urlRoute;
    if (internal) {
      route['routeType'] = routeType;
    }
    return route;

  }


  getSchemaRoutes(internal, apiPrefix) {

    let schemaRoutes = [];

    for (var tableName in this.metaDb.tables) {

      let routes = []
      let tableObj = {}

      let table = this.metaDb.tables[tableName];

      tableObj['resource'] = tableName;

      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/describe', 'describe'))
      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/count', 'count'))
      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/groupby', 'groupby'))
      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/aggregate', 'aggregate'))
      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/findOne', 'findOne'))
      routes.push(this.prepareRoute(internal, 'post', apiPrefix, tableName, 'create'))
      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName, 'list'))
      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/:id', 'read'))
      routes.push(this.prepareRoute(internal, 'put', apiPrefix, tableName + '/:id', 'update'))
      routes.push(this.prepareRoute(internal, 'delete', apiPrefix, tableName + '/:id', 'delete'))
      routes.push(this.prepareRoute(internal, 'get', apiPrefix, tableName + '/:id/exists', 'exists'))

      for (var j = 0; j < table['foreignKeys'].length; ++j) {
        let fk = table['foreignKeys'][j]
        routes.push(this.prepareRoute(internal, 'get', apiPrefix, fk['referenced_table_name'] + '/:id/' + fk['table_name'], 'relational'))
      }

      tableObj['routes'] = routes;

      schemaRoutes.push(tableObj);

    }

    return schemaRoutes;

  }

  globalRoutesPrint(apiPrefix) {

    let r = []

    r.push(apiPrefix + "tables")

    if (this.sqlConfig.dynamic) {
      r.push(apiPrefix + "dynamic")
      r.push("/upload")
      r.push("/uploads")
      r.push("/download")
    }


    return r;

  }

}


//expose class
module.exports = Xsql;


function getDataType(colType, typesArr) {
  // console.log(colType,typesArr);
  for (let i = 0; i < typesArr.length; ++i) {
    if (colType.indexOf(typesArr[i]) !== -1) {
      return 1;
    }
  }
  return 0;
}

function getColumnType(column) {

  let strTypes = ['varchar', 'text', 'char', 'tinytext', 'mediumtext', 'longtext', 'blob', 'mediumblob', 'longblob', 'tinyblob', 'binary', 'varbinary'];
  let intTypes = ['int', 'long', 'smallint', 'mediumint', 'bigint', 'tinyint'];
  let flatTypes = ['float', 'double', 'decimal'];
  let dateTypes = ['date', 'datetime', 'timestamp', 'time', 'year'];

  //console.log(column);
  if (getDataType(column['data_type'], strTypes)) {
    return "string"
  } else if (getDataType(column['data_type'], intTypes)) {
    return "int"
  } else if (getDataType(column['data_type'], flatTypes)) {
    return "float"
  } else if (getDataType(column['data_type'], dateTypes)) {
    return "date"
  } else {
    return "string"
  }

}