'use strict';

const ResultSet = require('./resultset');
const FieldDetail = require('../const/field-detail');
const FieldType = require('../const/field-type');
const Long = require('long');
const moment = require('moment-timezone');
const QUOTE = 0x27;

class CommonText extends ResultSet {
  constructor(resolve, reject, cmdOpts, connOpts, sql, values) {
    super(resolve, reject);
    this.configAssign(connOpts, cmdOpts);
    this.sql = sql;
    this.initialValues = values;
    this.getDateQuote = this.opts.tz
      ? this.opts.tz === 'Etc/UTC'
        ? CommonText.getUtcDate
        : CommonText.getTimezoneDate
      : CommonText.getLocalDate;
  }

  /**
   * Write (and escape) current parameter value to output writer
   *
   * @param out     output writer
   * @param value   current parameter
   * @param opts    connection options
   * @param info    connection information
   */
  writeParam(out, value, opts, info) {
    switch (typeof value) {
      case 'boolean':
        out.writeStringAscii(value ? 'true' : 'false');
        break;
      case 'bigint':
      case 'number':
        out.writeStringAscii('' + value);
        break;
      case 'object':
        if (value === null) {
          out.writeStringAscii('NULL');
        } else if (Object.prototype.toString.call(value) === '[object Date]') {
          out.writeStringAscii(this.getDateQuote(value, opts));
        } else if (Buffer.isBuffer(value)) {
          out.writeStringAscii("_BINARY '");
          out.writeBufferEscape(value);
          out.writeInt8(QUOTE);
        } else if (typeof value.toSqlString === 'function') {
          out.writeStringEscapeQuote(String(value.toSqlString()));
        } else if (Long.isLong(value)) {
          out.writeStringAscii(value.toString());
        } else if (Array.isArray(value)) {
          if (opts.arrayParenthesis) {
            out.writeStringAscii('(');
          }
          for (let i = 0; i < value.length; i++) {
            if (i !== 0) out.writeStringAscii(',');
            this.writeParam(out, value[i], opts, info);
          }
          if (opts.arrayParenthesis) {
            out.writeStringAscii(')');
          }
        } else {
          if (
            value.type != null &&
            [
              'Point',
              'LineString',
              'Polygon',
              'MultiPoint',
              'MultiLineString',
              'MultiPolygon',
              'GeometryCollection'
            ].includes(value.type)
          ) {
            //GeoJSON format.
            let prefix =
              (info.isMariaDB() && info.hasMinVersion(10, 1, 4)) ||
              (!info.isMariaDB() && info.hasMinVersion(5, 7, 6))
                ? 'ST_'
                : '';
            switch (value.type) {
              case 'Point':
                out.writeStringAscii(
                  prefix +
                    "PointFromText('POINT(" +
                    CommonText.geoPointToString(value.coordinates) +
                    ")')"
                );
                break;

              case 'LineString':
                out.writeStringAscii(
                  prefix +
                    "LineFromText('LINESTRING(" +
                    CommonText.geoArrayPointToString(value.coordinates) +
                    ")')"
                );
                break;

              case 'Polygon':
                out.writeStringAscii(
                  prefix +
                    "PolygonFromText('POLYGON(" +
                    CommonText.geoMultiArrayPointToString(value.coordinates) +
                    ")')"
                );
                break;

              case 'MultiPoint':
                out.writeStringAscii(
                  prefix +
                    "MULTIPOINTFROMTEXT('MULTIPOINT(" +
                    CommonText.geoArrayPointToString(value.coordinates) +
                    ")')"
                );
                break;

              case 'MultiLineString':
                out.writeStringAscii(
                  prefix +
                    "MLineFromText('MULTILINESTRING(" +
                    CommonText.geoMultiArrayPointToString(value.coordinates) +
                    ")')"
                );
                break;

              case 'MultiPolygon':
                out.writeStringAscii(
                  prefix +
                    "MPolyFromText('MULTIPOLYGON(" +
                    CommonText.geoMultiPolygonToString(value.coordinates) +
                    ")')"
                );
                break;

              case 'GeometryCollection':
                out.writeStringAscii(
                  prefix +
                    "GeomCollFromText('GEOMETRYCOLLECTION(" +
                    CommonText.geometricCollectionToString(value.geometries) +
                    ")')"
                );
                break;
            }
          } else {
            if (opts.permitSetMultiParamEntries) {
              let first = true;
              for (let key in value) {
                const val = value[key];
                if (typeof val === 'function') continue;
                if (first) {
                  first = false;
                } else {
                  out.writeStringAscii(',');
                }
                out.writeString('`' + key + '`');
                out.writeStringAscii('=');
                this.writeParam(out, val, opts, info);
              }
              if (first) out.writeStringEscapeQuote(JSON.stringify(value));
            } else {
              out.writeStringEscapeQuote(JSON.stringify(value));
            }
          }
        }
        break;
      default:
        out.writeStringEscapeQuote(value);
    }
  }

  static geometricCollectionToString(geo) {
    if (!geo) return '';
    let st = '';
    for (let i = 0; i < geo.length; i++) {
      //GeoJSON format.
      st += i !== 0 ? ',' : '';
      switch (geo[i].type) {
        case 'Point':
          st += 'POINT(' + CommonText.geoPointToString(geo[i].coordinates) + ')';
          break;

        case 'LineString':
          st += 'LINESTRING(' + CommonText.geoArrayPointToString(geo[i].coordinates) + ')';
          break;

        case 'Polygon':
          st += 'POLYGON(' + CommonText.geoMultiArrayPointToString(geo[i].coordinates) + ')';
          break;

        case 'MultiPoint':
          st += 'MULTIPOINT(' + CommonText.geoArrayPointToString(geo[i].coordinates) + ')';
          break;

        case 'MultiLineString':
          st +=
            'MULTILINESTRING(' + CommonText.geoMultiArrayPointToString(geo[i].coordinates) + ')';
          break;

        case 'MultiPolygon':
          st += 'MULTIPOLYGON(' + CommonText.geoMultiPolygonToString(geo[i].coordinates) + ')';
          break;
      }
    }
    return st;
  }

  static geoMultiPolygonToString(coords) {
    if (!coords) return '';
    let st = '';
    for (let i = 0; i < coords.length; i++) {
      st += (i !== 0 ? ',(' : '(') + CommonText.geoMultiArrayPointToString(coords[i]) + ')';
    }
    return st;
  }

  static geoMultiArrayPointToString(coords) {
    if (!coords) return '';
    let st = '';
    for (let i = 0; i < coords.length; i++) {
      st += (i !== 0 ? ',(' : '(') + CommonText.geoArrayPointToString(coords[i]) + ')';
    }
    return st;
  }

  static geoArrayPointToString(coords) {
    if (!coords) return '';
    let st = '';
    for (let i = 0; i < coords.length; i++) {
      st += (i !== 0 ? ',' : '') + CommonText.geoPointToString(coords[i]);
    }
    return st;
  }

  static geoPointToString(coords) {
    if (!coords) return '';
    return (isNaN(coords[0]) ? '' : coords[0]) + ' ' + (isNaN(coords[1]) ? '' : coords[1]);
  }

  parseRowAsArray(columns, packet, connOpts) {
    const row = new Array(this._columnCount);
    for (let i = 0; i < this._columnCount; i++) {
      row[i] = this._getValue(i, columns[i], this.opts, connOpts, packet);
    }
    return row;
  }

  parseRowNested(columns, packet, connOpts) {
    const row = {};
    for (let i = 0; i < this._columnCount; i++) {
      if (!row[this.tableHeader[i][0]]) row[this.tableHeader[i][0]] = {};
      row[this.tableHeader[i][0]][this.tableHeader[i][1]] = this._getValue(
        i,
        columns[i],
        this.opts,
        connOpts,
        packet
      );
    }
    return row;
  }

  parseRowStd(columns, packet, connOpts) {
    const row = {};
    for (let i = 0; i < this._columnCount; i++) {
      row[this.tableHeader[i]] = this._getValue(i, columns[i], this.opts, connOpts, packet);
    }
    return row;
  }

  castTextWrapper(column, opts, connOpts, packet) {
    column.string = () => packet.readStringLength();
    column.buffer = () => packet.readBufferLengthEncoded();
    column.float = () => packet.readFloatLengthCoded();
    column.int = () => packet.readIntLengthEncoded();
    column.long = () =>
      packet.readLongLengthEncoded(
        opts.supportBigInt,
        opts.supportBigNumbers,
        opts.bigNumberStrings,
        (column.flags & FieldDetail.UNSIGNED) > 0
      );
    column.decimal = () => packet.readDecimalLengthEncoded(opts.bigNumberStrings);
    column.date = () => packet.readDateTime(opts);
    column.geometry = () => {
      return column.readGeometry();
    };
  }

  readCastValue(index, column, opts, connOpts, packet) {
    this.castTextWrapper(column, opts, connOpts, packet);
    return opts.typeCast(
      column,
      this.readRowData.bind(this, index, column, opts, connOpts, packet)
    );
  }

  /**
   * Read row data.
   *
   * @param index     current data index in row
   * @param column    associate metadata
   * @param opts   query options
   * @param connOpts  connection options
   * @param packet    row packet
   * @returns {*}     data
   */
  readRowData(index, column, opts, connOpts, packet) {
    switch (column.columnType) {
      case FieldType.TINY:
      case FieldType.SHORT:
      case FieldType.LONG:
      case FieldType.INT24:
      case FieldType.YEAR:
        return packet.readIntLengthEncoded();
      case FieldType.FLOAT:
      case FieldType.DOUBLE:
        return packet.readFloatLengthCoded();
      case FieldType.LONGLONG:
        return packet.readLongLengthEncoded(
          opts.supportBigInt,
          opts.supportBigNumbers,
          opts.bigNumberStrings,
          (column.flags & FieldDetail.UNSIGNED) > 0
        );
      case FieldType.DECIMAL:
      case FieldType.NEWDECIMAL:
        return packet.readDecimalLengthEncoded(opts.bigNumberStrings);
      case FieldType.DATE:
        if (opts.dateStrings) {
          return packet.readAsciiStringLengthEncoded();
        }
        return packet.readDate();
      case FieldType.DATETIME:
      case FieldType.TIMESTAMP:
        if (opts.dateStrings) {
          return packet.readAsciiStringLengthEncoded();
        }
        return packet.readDateTime(opts);
      case FieldType.TIME:
        return packet.readAsciiStringLengthEncoded();
      case FieldType.GEOMETRY:
        return packet.readGeometry(column.dataTypeName);
      case FieldType.JSON:
        //for mysql only => parse string as JSON object
        return JSON.parse(packet.readStringLengthEncoded('utf8'));

      default:
        if (column.dataTypeFormat && column.dataTypeFormat === 'json' && opts.autoJsonMap) {
          return JSON.parse(packet.readStringLengthEncoded('utf8'));
        }

        if (column.collation.index === 63) {
          return packet.readBufferLengthEncoded();
        }
        const string = packet.readStringLength();
        if (column.flags & 2048) {
          //SET
          return string == null ? null : string === '' ? [] : string.split(',');
        }
        return string;
    }
  }
}

function getDatePartQuote(year, mon, day, hour, min, sec, ms) {
  //return 'YYYY-MM-DD HH:MM:SS' datetime format
  //see https://mariadb.com/kb/en/library/datetime/
  return (
    "'" +
    (year > 999 ? year : year > 99 ? '0' + year : year > 9 ? '00' + year : '000' + year) +
    '-' +
    (mon < 10 ? '0' : '') +
    mon +
    '-' +
    (day < 10 ? '0' : '') +
    day +
    ' ' +
    (hour < 10 ? '0' : '') +
    hour +
    ':' +
    (min < 10 ? '0' : '') +
    min +
    ':' +
    (sec < 10 ? '0' : '') +
    sec +
    '.' +
    (ms > 99 ? ms : ms > 9 ? '0' + ms : '00' + ms) +
    "'"
  );
}

function getLocalDate(date, opts) {
  const year = date.getFullYear();
  const mon = date.getMonth() + 1;
  const day = date.getDate();
  const hour = date.getHours();
  const min = date.getMinutes();
  const sec = date.getSeconds();
  const ms = date.getMilliseconds();
  return getDatePartQuote(year, mon, day, hour, min, sec, ms);
}

function getUtcDate(date, opts) {
  const year = date.getUTCFullYear();
  const mon = date.getUTCMonth() + 1;
  const day = date.getUTCDate();
  const hour = date.getUTCHours();
  const min = date.getUTCMinutes();
  const sec = date.getUTCSeconds();
  const ms = date.getUTCMilliseconds();
  return getDatePartQuote(year, mon, day, hour, min, sec, ms);
}

function getTimezoneDate(date, opts) {
  if (date.getMilliseconds() != 0) {
    return moment.tz(date, opts.tz).format("'YYYY-MM-DD HH:mm:ss.SSS'");
  }
  return moment.tz(date, opts.tz).format("'YYYY-MM-DD HH:mm:ss'");
}

module.exports = CommonText;
module.exports.getTimezoneDate = getTimezoneDate;
module.exports.getUtcDate = getUtcDate;
module.exports.getLocalDate = getLocalDate;
