import {Driver, Connection, Statement, ResultSet, DatabaseMetaData, ColumnInfo, PropertyInfo} from "ndbc-connector";
import * as mysql from "mysql";
import {Utils} from "./src/uitls";

export class MySQLDriver implements Driver {
  connect(properties: Map<string, any>): Promise<Connection> {
    let connection:MySQLConnection = new MySQLConnection(properties);
    return connection.open();
  }
}

export class MySQLConnection implements Connection {
  private _properties: Map<string, any> = new Map<string, any>();
  public _connection!: mysql.Connection;
  constructor(private readonly properties: Map<string, any>) {
    this._properties = properties;
  }

  open(): Promise<Connection> {
    return new Promise<Connection>((resolve, rejects) => {
      try {
        let user: any = this.getProperty("user", true);
        let host: any = this.getProperty("host", true);
        let password: any = this.getProperty("password", true);
        let database: any = this.getProperty("database", true);
        let port: any = this.getProperty("port", false, 3306);
        var connection = mysql.createConnection({
          host     : host,
          user     : user,
          password : password,
          database : database,
          port     : port
        });
         
        connection.connect((error) => {
          rejects(error);
        });
  
        this._connection = connection;
        resolve(this);
      } catch (err) {
        rejects(err);
      }
    });
  }

  close(): void {
    this._connection.end();
  }

  createStatement(query: string): Statement {
    return new MySQLStatement(this, query);
  }

  getMetadata(): DatabaseMetaData {
    return new MySQLDatabaseMetaData(this);
  } 

  private getProperty(propertyName: string, required: boolean, defaultValue?: any): any {
    let value = this._properties.get(propertyName);
    if (value == undefined || !(typeof value === "string")) {
      if (required) {
        throw new Error(`The ${propertyName} is required.`);
      } else {
        value = defaultValue;
      }
    }

    return value;
  }
}

export class MySQLStatement implements Statement {
  constructor(private readonly connection: MySQLConnection, private readonly queryString: string) {
  }

  execute(parameters: Map<string, any>): Promise<ResultSet> {
    let values:any[] = [];
    parameters.forEach((value:any, key:string) => {
      values.push(value);
    });
    return new Promise((resolve, rejects) => this.connection._connection.query({sql: this.queryString, values: values}, (err, results, fields?: mysql.FieldInfo[]) => {
      if (err === undefined || err == null) {
        resolve(new MySQLResultSet(results, fields));
      } else {
        rejects(err);
      }
    }));
  }

  executeNonQuery(parameters: Map<string, any>): Promise<number> {
    let values:any[] = [];
    parameters.forEach((key:string, value:any) => {
      values.push(value);
    });
    return new Promise((resolve, rejects) => this.connection._connection.query({sql: this.queryString, values: values}, (err, results, fields?: mysql.FieldInfo[]) => {
      if (err === undefined || err == null) {
        resolve(1);
      } else {
        rejects(err);
      }
    }));
  }
}

export class MySQLDatabaseMetaData implements DatabaseMetaData {
  private _properties: PropertyInfo[];

  constructor(private readonly connection: MySQLConnection) {
    this._properties = [];   
  }
  getConnectionProperites(): PropertyInfo[] {
    if (this._properties.length === 0) {
      this._properties.push(new PropertyInfo("host", "string", "", ""))
      this._properties.push(new PropertyInfo("port", "string", 3306, ""))
      this._properties.push(new PropertyInfo("database", "string", "", ""))
      this._properties.push(new PropertyInfo("user", "string", "", ""))
      this._properties.push(new PropertyInfo("password", "string", "", ""))
    }

    return this._properties;
  }

  getClientInfoProperties(): Promise<ResultSet> {
    return new Promise<ResultSet>((resolve, rejects) => {
      resolve(new EmptyResultSet());
    })
  }

  getServerInfoProperties(): Promise<ResultSet> {
    return new Promise<ResultSet>((resolve, rejects) => {
      resolve(new EmptyResultSet());
    })
  }

  getCatalogs(catalog: string): Promise<ResultSet> {
    return new Promise<ResultSet>((resolve, rejects) => {
      resolve(new EmptyResultSet());
    })
  }

  async getSchemas(catalog: string, schema: string): Promise<ResultSet> {
    let statement:Statement = this.connection.createStatement("SHOW DATABASES");
    let results:ResultSet | undefined;
    let metadata:ColumnInfo[] = [];
    let rows:any[][] = [];
    try {
      results = await statement.execute(new Map());
      metadata.push(new ColumnInfo("TABLE_SCHEM", "string"));
      while(results.next()) {
        let row:any[] = [];
        row.push(results.getValue(0));
        rows.push(row);
      }
    } finally {
      if (results != undefined) {
        results.close();
      }
    }

    return new Promise<ResultSet>((resolve, rejects) => {
      resolve(new ValuesResultSet(metadata, rows));
    })
  }

  getTables(catalog: string, schema: string, table: string, types: string[]): Promise<ResultSet> {
    let query:string = "SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES";
    let whereClause:string = "";
    let parameters:Map<string, any> = new Map();
    if (!Utils.isNullOrEmpty(schema)) {
      whereClause += " WHERE TABLE_SCHEMA = ? "
      parameters.set("TABLE_SCHEMA", schema);
    }

    if (!Utils.isNullOrEmpty(table)) {
      if (whereClause === "") {
        whereClause += " WHERE ";
      } else {
        whereClause += " AND ";
      }

      whereClause += " TABLE_NAME = ? ";
      parameters.set("TABLE_NAME", table);
    }

    let statement:Statement = this.connection.createStatement(query + whereClause);
    return statement.execute(parameters);
  }

  getColumns(catalog: string, schema: string, table: string): Promise<ResultSet> {
    let query:string = "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, EXTRA, COLUMN_COMMENT " 
    + "FROM INFORMATION_SCHEMA.COLUMNS";
    let whereClause:string = "";
    let parameters:Map<string, any> = new Map();
    if (!Utils.isNullOrEmpty(schema)) {
      whereClause += " WHERE TABLE_SCHEMA = ? "
      parameters.set("TABLE_SCHEMA", schema);
    }

    if (!Utils.isNullOrEmpty(table)) {
      if (whereClause === "") {
        whereClause += " WHERE ";
      } else {
        whereClause += " AND ";
      }

      whereClause += " TABLE_NAME = ? ";
      parameters.set("TABLE_NAME", table);
    }
    let statement:Statement = this.connection.createStatement(query + whereClause);
    return statement.execute(parameters);
  }

  getProcedures(catalog: string, schema: string): Promise<ResultSet> {
    throw new Error("Method not implemented.");
  }

  getProcedureParameters(catalog: string, schema: string, procedure: string): Promise<ResultSet> {
    throw new Error("Method not implemented.");
  }
}

class EmptyResultSet implements ResultSet {
  close(): void {
  }

  next(): boolean {
    return false;
  }

  getValue(index: string | number) {
    return null;
  }

  getMetadata(): ColumnInfo[] {
    return [];
  }
}

class ResultSetBase implements ResultSet {
  protected _index:number;
  protected _count:number;

  constructor() {
    this._index = -1;
    this._count = 0;
  }

  close(): void {
  }

  getValue(index: string | number):any {
    return null;
  }

  getMetadata(): ColumnInfo[] {
    return [];
  }

  next(): boolean {
    if (this._count <= 0) {
      return false;
    }

    if (this._index < this._count - 1) {
      this._index ++;
      return true;
    } else {
      this._index = this._count;
      return false;
    }
  }

  protected ensureRange() {
    if (this._index < 0) {
      throw new Error("Please invoke next method first.");
    }

    if (this._index >= this._count) {
      throw new Error("The index is out of range.");
    }
  }
}

class ValuesResultSet extends ResultSetBase {
  private _columnNames:string[];

  constructor(private readonly columns:ColumnInfo[], private readonly results:any[][]){
    super();
    this._count = results.length;
    this._columnNames=[];
  }

  getValue(columnName: string | number) {
    super.ensureRange();
    let row:any[] = this.results[this._index];

    let index: number = -1;
    if (typeof columnName === "string") {
      if (this._columnNames.length == 0) {
        for (let index = 0; index < this.columns.length; index++) {
          const element = this.columns[index];
          this._columnNames.push(element.getColumnName());
        }
      }

      index = this._columnNames.indexOf(<string>columnName);
    } else {
      index = columnName as number;
    }

    if (index == -1) {
      throw Error(`Invalid of ${columnName} in getValue method`);
    }

    return row[index];
  }

  getMetadata(): ColumnInfo[] {
    return this.columns;
  }
}

class KeyValueResultSet extends ResultSetBase {
  private _metadata:ColumnInfo[];

  constructor(private readonly names:string[], private readonly values:string[]) {
    super();
    this._metadata = [];
    this._count = 1;
  }

  getValue(columnName: string | number) {
    this.ensureRange();
    let index: number = -1;
    if (typeof columnName === "string") {
      index = this.names.indexOf(columnName);
    } else {
      index = columnName as number;
    }

    if (index == -1) {
      throw Error(`Invalid of ${columnName} in getValue method`);
    }

    return this.values[index];
  }

  getMetadata(): ColumnInfo[] {
    if (this._metadata.length == 0) {
      for (let index = 0; index < this.names.length; index++) {
        const name = this.names[index];
        this._metadata.push(new ColumnInfo(name, "string"));
      }
    }

    return this._metadata;
  }
}

export class MySQLResultSet extends ResultSetBase {
  private _columns: ColumnInfo[] | undefined;
  private _indexes: string[] | undefined;
  private _currentRow: any | undefined;
  constructor(private readonly results: any, private readonly fields:  mysql.FieldInfo[] | undefined) {
    super();
    this._count = this.results === undefined ? 0 : this.results.length;
  }

  next(): boolean {
    let value:boolean = super.next();
    this._currentRow = undefined;
    return value;
  }

  getValue(columnName: string | number) {
    this.ensureRange();
    if (this._currentRow === undefined) {
      let rowObject: any = this.results[this._index];
      this._currentRow = rowObject;
    }

    if (this._currentRow === undefined) {
      return null;
    }

    let name: string | undefined;
    if (typeof columnName === "number") {
      if (this._indexes == undefined) {
        this.getMetadata();
      } 

      name = (<string[]>this._indexes)[<number>columnName];
    } else {
      name = columnName as string;
    }

    if (name !== undefined) {
      return this._currentRow[name];
    } else {
      return null;
    }
  }

  getMetadata(): ColumnInfo[] {
    if (this._columns === undefined) {
      this._columns = [];
      this._indexes = [];
      if (this.fields !== undefined) {
        this.fields.forEach(element => {
          if (this._columns != undefined) {
            let column:ColumnInfo = new ColumnInfo(element.name, element.type.toString());
            this._columns.push(column);
          }
 
          if (this._indexes != undefined) {
            this._indexes.push(element.name);
          }
        });
      }
    }

    return this._columns;
  }

  close():void {
  }
}