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

'use strict';

const FieldType = require('../../const/field-type');
const Errors = require('../../misc/errors');

module.exports.newRow = function (packet, columns) {
  packet.skip(1); // skip 0x00 header.
  const len = ~~((columns.length + 9) / 8);
  const nullBitMap = new Array(len);
  for (let i = 0; i < len; i++) nullBitMap[i] = packet.readUInt8();
  return nullBitMap;
};
module.exports.castWrapper = function (column, packet, opts, nullBitmap, index) {
  const isFixedWidth = BINARY_FIXED_WIDTH_TYPES.has(column.columnType);

  column.string = () =>
    isNullBitmap(index, nullBitmap)
      ? null
      : isFixedWidth
        ? readFixedWidthAsString(column, packet)
        : packet.readStringLengthEncoded();
  column.buffer = () =>
    isNullBitmap(index, nullBitmap)
      ? null
      : isFixedWidth
        ? Buffer.from(readFixedWidthAsString(column, packet), 'latin1')
        : packet.readBufferLengthEncoded();
  column.float = () =>
    isNullBitmap(index, nullBitmap)
      ? null
      : column.columnType === FieldType.DOUBLE
        ? packet.readDouble()
        : packet.readFloat();
  column.tiny = () =>
    isNullBitmap(index, nullBitmap) ? null : column.signed() ? packet.readInt8() : packet.readUInt8();
  column.short = () =>
    isNullBitmap(index, nullBitmap) ? null : column.signed() ? packet.readInt16() : packet.readUInt16();
  column.int = () =>
    isNullBitmap(index, nullBitmap) ? null : column.signed() ? packet.readInt32() : packet.readUInt32();
  column.long = () =>
    isNullBitmap(index, nullBitmap) ? null : column.signed() ? packet.readBigInt64() : packet.readBigUInt64();
  column.decimal = () => (isNullBitmap(index, nullBitmap) ? null : packet.readDecimalLengthEncoded());
  column.date = () => (isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDate(opts));
  column.datetime = () => (isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDateTime());

  column.geometry = () => {
    let defaultVal = null;
    if (column.dataTypeName) {
      switch (column.dataTypeName) {
        case 'point':
          defaultVal = { type: 'Point' };
          break;
        case 'linestring':
          defaultVal = { type: 'LineString' };
          break;
        case 'polygon':
          defaultVal = { type: 'Polygon' };
          break;
        case 'multipoint':
          defaultVal = { type: 'MultiPoint' };
          break;
        case 'multilinestring':
          defaultVal = { type: 'MultiLineString' };
          break;
        case 'multipolygon':
          defaultVal = { type: 'MultiPolygon' };
          break;
        default:
          defaultVal = { type: column.dataTypeName };
          break;
      }
    }

    if (isNullBitmap(index, nullBitmap)) {
      return defaultVal;
    }
    return packet.readGeometry(defaultVal);
  };
};
module.exports.parser = function (col, opts) {
  // set reader function read(col, packet, index, nullBitmap, opts, throwUnexpectedError)
  // this permit for multi-row result-set to avoid resolving type parsing each data.

  // return constant parser (function not depending on column info other than type)
  const defaultParser = col.signed()
    ? DEFAULT_SIGNED_PARSER_TYPE[col.columnType]
    : DEFAULT_UNSIGNED_PARSER_TYPE[col.columnType];
  if (defaultParser) return defaultParser;

  // parser depending on column info
  switch (col.columnType) {
    case FieldType.BIGINT:
      if (col.signed()) {
        return opts.bigIntAsNumber || opts.supportBigNumbers ? readBigintAsIntBinarySigned : readBigintBinarySigned;
      }
      return opts.bigIntAsNumber || opts.supportBigNumbers ? readBigintAsIntBinaryUnsigned : readBigintBinaryUnsigned;

    case FieldType.DATETIME:
    case FieldType.TIMESTAMP:
      return opts.dateStrings ? readTimestampStringBinary.bind(null, col.scale) : readTimestampBinary;

    case FieldType.DECIMAL:
    case FieldType.NEWDECIMAL:
      return col.scale === 0 ? readDecimalAsIntBinary : readDecimalBinary;

    case FieldType.GEOMETRY:
      let defaultVal = col.__getDefaultGeomVal();
      return readGeometryBinary.bind(null, defaultVal);

    case FieldType.BIT:
      if (col.columnLength === 1 && opts.bitOneIsBoolean) {
        return readBitBinaryBoolean;
      }
      return readBinaryBuffer;
    case FieldType.JSON:
      return opts.jsonStrings ? readStringBinary : readJsonBinary;

    default:
      if (col.dataTypeFormat && col.dataTypeFormat === 'json' && opts.autoJsonMap) {
        return readJsonBinary;
      }
      if (col.collation.index === 63) {
        return readBinaryBuffer;
      }
      if (col.isSet()) {
        return readBinarySet;
      }
      return readStringBinary;
  }
};

const isNullBitmap = (index, nullBitmap) => {
  return (nullBitmap[~~((index + 2) / 8)] & (1 << ((index + 2) % 8))) > 0;
};

// In the binary protocol, only variable-length values are length-encoded: fixed-width types
// (integers, floats and temporal types) are sent using their native representation. Reading one of
// those with a length-encoded reader would consume the first data byte as a length prefix, and so
// desynchronize the cursor for every remaining column of the row.
const BINARY_FIXED_WIDTH_TYPES = new Set([
  FieldType.TINY,
  FieldType.SHORT,
  FieldType.YEAR,
  FieldType.INT24,
  FieldType.INT,
  FieldType.BIGINT,
  FieldType.FLOAT,
  FieldType.DOUBLE,
  FieldType.DATE,
  FieldType.DATETIME,
  FieldType.TIMESTAMP,
  FieldType.TIME
]);

// `string()` must always return a string, whatever the `dateStrings` option is set to.
const DATE_AS_STRING = { dateStrings: true };

// Decode a fixed-width binary value, returning its textual form so that `string()`/`buffer()`
// return the same value the text protocol would.
const readFixedWidthAsString = (column, packet) => {
  switch (column.columnType) {
    case FieldType.TINY:
      return String(column.signed() ? packet.readInt8() : packet.readUInt8());

    case FieldType.YEAR:
    case FieldType.SHORT:
      return String(column.signed() ? packet.readInt16() : packet.readUInt16());

    case FieldType.INT24: {
      const val = column.signed() ? packet.readInt24() : packet.readUInt24();
      packet.skip(1); // MEDIUMINT is encoded on 4 bytes in exchanges !
      return String(val);
    }

    case FieldType.INT:
      return String(column.signed() ? packet.readInt32() : packet.readUInt32());

    case FieldType.BIGINT:
      return (column.signed() ? packet.readBigInt64() : packet.readBigUInt64()).toString();

    case FieldType.FLOAT:
      return String(packet.readFloat());

    case FieldType.DOUBLE:
      return String(packet.readDouble());

    case FieldType.DATE:
      return packet.readBinaryDate(DATE_AS_STRING);

    case FieldType.DATETIME:
    case FieldType.TIMESTAMP:
      return packet.readBinaryDateTimeAsString(column.scale);

    default: // FieldType.TIME
      return packet.readBinaryTime();
  }
};

const readTinyBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readInt8();
const readTinyBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readUInt8();
const readShortBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readInt16();
const readShortBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readUInt16();
const readMediumBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
  if (isNullBitmap(index, nullBitmap)) {
    return null;
  }
  const result = packet.readInt24();
  packet.skip(1); // MEDIUMINT is encoded on 4 bytes in exchanges !
  return result;
};
const readMediumBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
  if (isNullBitmap(index, nullBitmap)) {
    return null;
  }
  const result = packet.readUInt24();
  packet.skip(1); // MEDIUMINT is encoded on 4 bytes in exchanges !
  return result;
};
const readIntBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readInt32();
const readIntBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readUInt32();
const readFloatBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readFloat();
const readDoubleBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readDouble();
const readBigintBinaryUnsigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
  if (isNullBitmap(index, nullBitmap)) return null;
  return packet.readBigUInt64();
};
const readBigintBinarySigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
  if (isNullBitmap(index, nullBitmap)) return null;
  return packet.readBigInt64();
};

const readBigintAsIntBinaryUnsigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
  if (isNullBitmap(index, nullBitmap)) return null;
  const val = packet.readBigUInt64();
  if (opts.bigIntAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(val))) {
    return throwUnexpectedError(
      `value ${val} can't safely be converted to number`,
      false,
      null,
      '42000',
      Errors.ER_PARSING_PRECISION
    );
  }
  if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(val)))) {
    return val.toString();
  }
  return Number(val);
};

const readBigintAsIntBinarySigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
  if (isNullBitmap(index, nullBitmap)) return null;
  const val = packet.readBigInt64();
  if (opts.bigIntAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(val))) {
    return throwUnexpectedError(
      `value ${val} can't safely be converted to number`,
      false,
      null,
      '42000',
      Errors.ER_PARSING_PRECISION
    );
  }
  if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(val)))) {
    return val.toString();
  }
  return Number(val);
};

const readGeometryBinary = (defaultVal, packet, opts, throwUnexpectedError, nullBitmap, index) => {
  if (isNullBitmap(index, nullBitmap)) {
    return defaultVal;
  }
  return packet.readGeometry(defaultVal);
};
const readDateBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDate(opts);
const readTimestampBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDateTime();
const readTimestampStringBinary = (scale, packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDateTimeAsString(scale);
const readTimeBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readBinaryTime();
const readDecimalAsIntBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
  //checkNumberRange additional check is only done when
  // resulting value is an integer
  if (isNullBitmap(index, nullBitmap)) return null;
  const valDec = packet.readDecimalLengthEncoded();
  if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
    if (opts.decimalAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(valDec))) {
      return throwUnexpectedError(
        `value ${valDec} can't safely be converted to number`,
        false,
        null,
        '42000',
        Errors.ER_PARSING_PRECISION
      );
    }
    if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(valDec)))) {
      return valDec;
    }
    return Number(valDec);
  }
  return valDec;
};
const readDecimalBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
  if (isNullBitmap(index, nullBitmap)) return null;
  const valDec = packet.readDecimalLengthEncoded();
  if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
    const numberValue = Number(valDec);
    if (
      opts.supportBigNumbers &&
      (opts.bigNumberStrings || (Number.isInteger(numberValue) && !Number.isSafeInteger(numberValue)))
    ) {
      return valDec;
    }
    return numberValue;
  }
  return valDec;
};
const readJsonBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : JSON.parse(packet.readStringLengthEncoded());
const readBitBinaryBoolean = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded()[0] === 1;
const readBinaryBuffer = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded();
const readBinarySet = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
  if (isNullBitmap(index, nullBitmap)) return null;
  const string = packet.readStringLengthEncoded();
  return string == null ? null : string === '' ? [] : string.split(',');
};
const readStringBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
  isNullBitmap(index, nullBitmap) ? null : packet.readStringLengthEncoded();

const DEFAULT_SIGNED_PARSER_TYPE = Array(256);
DEFAULT_SIGNED_PARSER_TYPE[FieldType.TINY] = readTinyBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.YEAR] = readShortBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.SHORT] = readShortBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.INT24] = readMediumBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.INT] = readIntBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.FLOAT] = readFloatBinary;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.DOUBLE] = readDoubleBinary;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.DATE] = readDateBinary;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.TIME] = readTimeBinary;

const DEFAULT_UNSIGNED_PARSER_TYPE = Array(256);
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.TINY] = readTinyBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.YEAR] = readShortBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.SHORT] = readShortBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.INT24] = readMediumBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.INT] = readIntBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.FLOAT] = readFloatBinary;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.DOUBLE] = readDoubleBinary;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.DATE] = readDateBinary;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.TIME] = readTimeBinary;
