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

'use strict';

class BinaryEncoder {
  /**
   * 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
   */
  static writeParam(out, value, opts, info) {
    // GEOJSON are not checked, because change to null/Buffer on parameter validation
    switch (typeof value) {
      case 'boolean':
        out.writeInt8(value ? 0x01 : 0x00);
        break;
      case 'bigint':
        if (value >= 2n ** 63n) {
          out.writeLengthEncodedString(value.toString());
        } else {
          out.writeBigInt(value);
        }
        break;

      case 'number':
        // additional verification, to permit query without type,
        // like 'SELECT ?' returning same type of value
        if (Number.isInteger(value) && value >= -2147483648 && value < 2147483647) {
          out.writeInt32(value);
          break;
        }
        out.writeDouble(value);
        break;
      case 'string':
        out.writeLengthEncodedString(value);
        break;
      case 'object':
        if (Object.prototype.toString.call(value) === '[object Date]') {
          out.writeBinaryDate(value);
        } else if (Buffer.isBuffer(value)) {
          out.writeLengthEncodedBuffer(value);
        } else if (typeof value.toSqlString === 'function') {
          out.writeLengthEncodedString(String(value.toSqlString()));
        } else {
          out.writeLengthEncodedString(JSON.stringify(value));
        }
        break;
      default:
        out.writeLengthEncodedBuffer(value);
    }
  }

  static getBufferFromGeometryValue(value, headerType) {
    let geoBuff;
    let pos;
    let type;
    if (value == null) return null;
    if (!headerType) {
      switch (value.type) {
        case 'Point':
          geoBuff = Buffer.allocUnsafe(21);
          geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
          geoBuff.writeInt32LE(1, 1); //wkbPoint
          if (
            value.coordinates &&
            Array.isArray(value.coordinates) &&
            value.coordinates.length >= 2 &&
            !isNaN(value.coordinates[0]) &&
            !isNaN(value.coordinates[1])
          ) {
            geoBuff.writeDoubleLE(value.coordinates[0], 5); //X
            geoBuff.writeDoubleLE(value.coordinates[1], 13); //Y
            return geoBuff;
          } else {
            return null;
          }

        case 'LineString':
          if (value.coordinates && Array.isArray(value.coordinates)) {
            const pointNumber = value.coordinates.length;
            geoBuff = Buffer.allocUnsafe(9 + 16 * pointNumber);
            geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
            geoBuff.writeInt32LE(2, 1); //wkbLineString
            geoBuff.writeInt32LE(pointNumber, 5);
            for (let i = 0; i < pointNumber; i++) {
              if (
                value.coordinates[i] &&
                Array.isArray(value.coordinates[i]) &&
                value.coordinates[i].length >= 2 &&
                !isNaN(value.coordinates[i][0]) &&
                !isNaN(value.coordinates[i][1])
              ) {
                geoBuff.writeDoubleLE(value.coordinates[i][0], 9 + 16 * i); //X
                geoBuff.writeDoubleLE(value.coordinates[i][1], 17 + 16 * i); //Y
              } else {
                return null;
              }
            }
            return geoBuff;
          } else {
            return null;
          }

        case 'Polygon':
          if (value.coordinates && Array.isArray(value.coordinates)) {
            const numRings = value.coordinates.length;
            let size = 0;
            for (let i = 0; i < numRings; i++) {
              if (!Array.isArray(value.coordinates[i])) return null;
              size += 4 + 16 * value.coordinates[i].length;
            }
            geoBuff = Buffer.allocUnsafe(9 + size);
            geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
            geoBuff.writeInt32LE(3, 1); //wkbPolygon
            geoBuff.writeInt32LE(numRings, 5);
            pos = 9;
            for (let i = 0; i < numRings; i++) {
              const lineString = value.coordinates[i];
              geoBuff.writeInt32LE(lineString.length, pos);
              pos += 4;
              for (let j = 0; j < lineString.length; j++) {
                if (
                  lineString[j] &&
                  Array.isArray(lineString[j]) &&
                  lineString[j].length >= 2 &&
                  !isNaN(lineString[j][0]) &&
                  !isNaN(lineString[j][1])
                ) {
                  geoBuff.writeDoubleLE(lineString[j][0], pos); //X
                  geoBuff.writeDoubleLE(lineString[j][1], pos + 8); //Y
                  pos += 16;
                } else {
                  return null;
                }
              }
            }
            return geoBuff;
          } else {
            return null;
          }

        case 'MultiPoint':
          type = 'MultiPoint';
          geoBuff = Buffer.allocUnsafe(9);
          geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
          geoBuff.writeInt32LE(4, 1); //wkbMultiPoint
          break;

        case 'MultiLineString':
          type = 'MultiLineString';
          geoBuff = Buffer.allocUnsafe(9);
          geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
          geoBuff.writeInt32LE(5, 1); //wkbMultiLineString
          break;

        case 'MultiPolygon':
          type = 'MultiPolygon';
          geoBuff = Buffer.allocUnsafe(9);
          geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
          geoBuff.writeInt32LE(6, 1); //wkbMultiPolygon
          break;

        case 'GeometryCollection':
          geoBuff = Buffer.allocUnsafe(9);
          geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
          geoBuff.writeInt32LE(7, 1); //wkbGeometryCollection

          if (value.geometries && Array.isArray(value.geometries)) {
            const coordinateLength = value.geometries.length;
            const subArrays = [geoBuff];
            for (let i = 0; i < coordinateLength; i++) {
              const tmpBuf = this.getBufferFromGeometryValue(value.geometries[i]);
              if (tmpBuf === null) break;
              subArrays.push(tmpBuf);
            }
            geoBuff.writeInt32LE(subArrays.length - 1, 5);
            return Buffer.concat(subArrays);
          } else {
            geoBuff.writeInt32LE(0, 5);
            return geoBuff;
          }
        default:
          return null;
      }
      if (value.coordinates && Array.isArray(value.coordinates)) {
        const coordinateLength = value.coordinates.length;
        const subArrays = [geoBuff];
        for (let i = 0; i < coordinateLength; i++) {
          const tmpBuf = this.getBufferFromGeometryValue(value.coordinates[i], type);
          if (tmpBuf === null) break;
          subArrays.push(tmpBuf);
        }
        geoBuff.writeInt32LE(subArrays.length - 1, 5);
        return Buffer.concat(subArrays);
      } else {
        geoBuff.writeInt32LE(0, 5);
        return geoBuff;
      }
    } else {
      switch (headerType) {
        case 'MultiPoint':
          if (value && Array.isArray(value) && value.length >= 2 && !isNaN(value[0]) && !isNaN(value[1])) {
            geoBuff = Buffer.allocUnsafe(21);
            geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
            geoBuff.writeInt32LE(1, 1); //wkbPoint
            geoBuff.writeDoubleLE(value[0], 5); //X
            geoBuff.writeDoubleLE(value[1], 13); //Y
            return geoBuff;
          }
          return null;

        case 'MultiLineString':
          if (value && Array.isArray(value)) {
            const pointNumber = value.length;
            geoBuff = Buffer.allocUnsafe(9 + 16 * pointNumber);
            geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
            geoBuff.writeInt32LE(2, 1); //wkbLineString
            geoBuff.writeInt32LE(pointNumber, 5);
            for (let i = 0; i < pointNumber; i++) {
              if (
                value[i] &&
                Array.isArray(value[i]) &&
                value[i].length >= 2 &&
                !isNaN(value[i][0]) &&
                !isNaN(value[i][1])
              ) {
                geoBuff.writeDoubleLE(value[i][0], 9 + 16 * i); //X
                geoBuff.writeDoubleLE(value[i][1], 17 + 16 * i); //Y
              } else {
                return null;
              }
            }
            return geoBuff;
          }
          return null;

        case 'MultiPolygon':
          if (value && Array.isArray(value)) {
            const numRings = value.length;
            let size = 0;
            for (let i = 0; i < numRings; i++) {
              // reject non-array rings before reserving space for them, so no byte
              // of the allocation can be left uninitialized by the writing loop
              if (!Array.isArray(value[i])) return null;
              size += 4 + 16 * value[i].length;
            }
            geoBuff = Buffer.allocUnsafe(9 + size);
            geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
            geoBuff.writeInt32LE(3, 1); //wkbPolygon
            geoBuff.writeInt32LE(numRings, 5);
            pos = 9;
            for (let i = 0; i < numRings; i++) {
              const lineString = value[i];
              geoBuff.writeInt32LE(lineString.length, pos);
              pos += 4;
              for (let j = 0; j < lineString.length; j++) {
                if (
                  lineString[j] &&
                  Array.isArray(lineString[j]) &&
                  lineString[j].length >= 2 &&
                  !isNaN(lineString[j][0]) &&
                  !isNaN(lineString[j][1])
                ) {
                  geoBuff.writeDoubleLE(lineString[j][0], pos); //X
                  geoBuff.writeDoubleLE(lineString[j][1], pos + 8); //Y
                  pos += 16;
                } else {
                  return null;
                }
              }
            }
            return geoBuff;
          }
          return null;
      }
      return null;
    }
  }
}

export default BinaryEncoder;
