//  SPDX-License-Identifier: LGPL-2.1-or-later
//  Copyright (c) 2015-2024 MariaDB Corporation Ab

'use strict';

const QUOTE = 0x27;

// Cache common GeoJSON types
const GEO_TYPES = new Set([
  'Point',
  'LineString',
  'Polygon',
  'MultiPoint',
  'MultiLineString',
  'MultiPolygon',
  'GeometryCollection'
]);

// Optimized function to pad numbers with leading zeros
const formatDigit = function (val, significantDigit) {
  const str = `${val}`;
  return str.length < significantDigit ? '0'.repeat(significantDigit - str.length) + str : str;
};

class TextEncoder {
  /**
   * Escape an identifier, doubling any backtick it contains.
   *
   * Object keys expanded by permitSetMultiParamEntries become column names, so
   * a key holding a backtick would otherwise close the identifier and let the
   * rest of the key be parsed as SQL.
   *
   * @param value identifier to escape
   * @returns {string} backtick-quoted identifier
   */
  static escapeId(value) {
    // always return escaped value, even when there are no special characters
    // to permit working with reserved words
    return '`' + value.replace(/`/g, '``') + '`';
  }

  /**
   * Write (and escape) current parameter value to output writer
   *
   * @param out     output writer
   * @param value   current parameter. Expected to be non-null
   * @param opts    connection options
   * @param info    connection information
   */
  static 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 'string':
        out.writeStringEscapeQuote(value);
        break;
      case 'object':
        if (Object.prototype.toString.call(value) === '[object Date]') {
          out.writeStringAscii(TextEncoder.getLocalDate(value));
        } 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 (Array.isArray(value)) {
          if (opts.arrayParenthesis) {
            out.writeStringAscii('(');
          }
          for (let i = 0; i < value.length; i++) {
            if (i !== 0) out.writeStringAscii(',');
            if (value[i] == null) {
              out.writeStringAscii('NULL');
            } else TextEncoder.writeParam(out, value[i], opts, info);
          }

          if (opts.arrayParenthesis) {
            out.writeStringAscii(')');
          }
        } else {
          if (value.type != null && GEO_TYPES.has(value.type)) {
            //GeoJSON format.
            const isMariaDb = info.isMariaDB();
            const prefix =
              (isMariaDb && info.hasMinVersion(10, 1, 4)) || (!isMariaDb && info.hasMinVersion(5, 7, 6)) ? 'ST_' : '';

            switch (value.type) {
              case 'Point':
                out.writeStringAscii(
                  prefix + "PointFromText('POINT(" + TextEncoder.geoPointToString(value.coordinates) + ")')"
                );
                break;

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

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

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

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

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

              case 'GeometryCollection':
                out.writeStringAscii(
                  prefix +
                    "GeomCollFromText('GEOMETRYCOLLECTION(" +
                    TextEncoder.geometricCollectionToString(value.geometries) +
                    ")')"
                );
                break;
            }
          } else if (String === value.constructor) {
            out.writeStringEscapeQuote(value);
            break;
          } else {
            if (opts.permitSetMultiParamEntries) {
              let first = true;
              for (const key in value) {
                const val = value[key];
                if (typeof val === 'function') continue;

                if (first) {
                  first = false;
                } else {
                  out.writeStringAscii(',');
                }

                out.writeString(TextEncoder.escapeId(key));

                if (val == null) {
                  out.writeStringAscii('=NULL');
                } else {
                  out.writeStringAscii('=');
                  TextEncoder.writeParam(out, val, opts, info);
                }
              }
              if (first) out.writeStringEscapeQuote(JSON.stringify(value));
            } else {
              out.writeStringEscapeQuote(JSON.stringify(value));
            }
          }
        }
        break;
    }
  }

  static geometricCollectionToString(geo) {
    if (!geo) return '';

    const len = geo.length;
    let st = '';

    for (let i = 0; i < len; i++) {
      const item = geo[i];
      //GeoJSON format.
      if (i !== 0) st += ',';

      switch (item.type) {
        case 'Point':
          st += `POINT(${TextEncoder.geoPointToString(item.coordinates)})`;
          break;

        case 'LineString':
          st += `LINESTRING(${TextEncoder.geoArrayPointToString(item.coordinates)})`;
          break;

        case 'Polygon':
          st += `POLYGON(${TextEncoder.geoMultiArrayPointToString(item.coordinates)})`;
          break;

        case 'MultiPoint':
          st += `MULTIPOINT(${TextEncoder.geoArrayPointToString(item.coordinates)})`;
          break;

        case 'MultiLineString':
          st += `MULTILINESTRING(${TextEncoder.geoMultiArrayPointToString(item.coordinates)})`;
          break;

        case 'MultiPolygon':
          st += `MULTIPOLYGON(${TextEncoder.geoMultiPolygonToString(item.coordinates)})`;
          break;
      }
    }
    return st;
  }

  static geoMultiPolygonToString(coords) {
    if (!coords) return '';

    const len = coords.length;
    if (len === 0) return '';

    let st = '(';

    for (let i = 0; i < len; i++) {
      if (i !== 0) st += ',(';
      st += TextEncoder.geoMultiArrayPointToString(coords[i]) + ')';
    }

    return st;
  }

  static geoMultiArrayPointToString(coords) {
    if (!coords) return '';

    const len = coords.length;
    if (len === 0) return '';

    let st = '(';

    for (let i = 0; i < len; i++) {
      if (i !== 0) st += ',(';
      st += TextEncoder.geoArrayPointToString(coords[i]) + ')';
    }

    return st;
  }

  static geoArrayPointToString(coords) {
    if (!coords) return '';

    const len = coords.length;
    if (len === 0) return '';

    let st = '';

    for (let i = 0; i < len; i++) {
      if (i !== 0) st += ',';
      st += TextEncoder.geoPointToString(coords[i]);
    }

    return st;
  }

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

  static getLocalDate(date) {
    const year = date.getFullYear();
    const month = date.getMonth() + 1;
    const day = date.getDate();
    const hours = date.getHours();
    const minutes = date.getMinutes();
    const seconds = date.getSeconds();
    const ms = date.getMilliseconds();

    const d = "'" + year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;

    if (ms === 0) return d + "'";

    return d + '.' + (ms < 10 ? '00' : ms < 100 ? '0' : '') + ms + "'";
  }

  static getFixedFormatDate(date) {
    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();

    let result =
      "'" +
      formatDigit(year, 4) +
      '-' +
      formatDigit(mon, 2) +
      '-' +
      formatDigit(day, 2) +
      ' ' +
      formatDigit(hour, 2) +
      ':' +
      formatDigit(min, 2) +
      ':' +
      formatDigit(sec, 2);

    if (ms > 0) {
      result += '.' + formatDigit(ms, 3);
    }

    return result + "'";
  }
}

module.exports = TextEncoder;
