'use strict';

const Packet = require('../packets/packet');
const StringParser = require('../parsers/string');
const CharsetToEncoding = require('../constants/charset_encodings.js');

const fields = ['catalog', 'schema', 'table', 'orgTable', 'name', 'orgName'];

// creating JS string is relatively expensive (compared to
// reading few bytes from buffer) because all string properties
// except for name are unlikely to be used we postpone
// string conversion until property access
//
// TODO: watch for integration benchmarks (one with real network buffer)
// there could be bad side effect as keeping reference to a buffer makes it
// sit in the memory longer (usually until final .query() callback)
// Latest v8 perform much better in regard to bufferer -> string conversion,
// at some point of time this optimisation might become unnecessary
// see https://github.com/sidorares/node-mysql2/pull/137
//
class ColumnDefinition {
  constructor(packet, clientEncoding, mariadbExtendedMetadata) {
    this._buf = packet.buffer;
    this._clientEncoding = clientEncoding;
    this._catalogLength = packet.readLengthCodedNumber();
    this._catalogStart = packet.offset;
    packet.offset += this._catalogLength;
    this._schemaLength = packet.readLengthCodedNumber();
    this._schemaStart = packet.offset;
    packet.offset += this._schemaLength;
    this._tableLength = packet.readLengthCodedNumber();
    this._tableStart = packet.offset;
    packet.offset += this._tableLength;
    this._orgTableLength = packet.readLengthCodedNumber();
    this._orgTableStart = packet.offset;
    packet.offset += this._orgTableLength;
    // name is always used, don't make it lazy
    const _nameLength = packet.readLengthCodedNumber();
    const _nameStart = packet.offset;
    packet.offset += _nameLength;
    this._orgNameLength = packet.readLengthCodedNumber();
    this._orgNameStart = packet.offset;
    packet.offset += this._orgNameLength;
    // MariaDB-specific column types (UUID, INET4, INET6, VECTOR and the JSON
    // alias) are sent as standard string/blob types on the wire; the real
    // type is only visible here, in the extended metadata block negotiated
    // via the MARIADB_CLIENT_EXTENDED_METADATA capability (parsed out of
    // line and with prototype defaults to keep this hot constructor small)
    if (mariadbExtendedMetadata) {
      this._parseMariadbExtendedMetadata(packet);
    }
    packet.skip(1); //  length of the following fields (always 0x0c)
    this.characterSet = packet.readInt16();
    this.encoding = CharsetToEncoding[this.characterSet];
    this.name = StringParser.decode(
      this._buf,
      this.encoding === 'binary' ? this._clientEncoding : this.encoding,
      _nameStart,
      _nameStart + _nameLength
    );
    this.columnLength = packet.readInt32();
    this.columnType = packet.readInt8();
    this.type = this.columnType;
    this.flags = packet.readInt16();
    this.decimals = packet.readInt8();
  }

  _parseMariadbExtendedMetadata(packet) {
    // length-encoded block of { int<1> id, string<lenenc> value } pairs
    const extendedMetadataLength = packet.readLengthCodedNumber() || 0;
    const extendedMetadataEnd = Math.min(
      packet.offset + extendedMetadataLength,
      packet.end
    );
    while (packet.offset < extendedMetadataEnd) {
      const id = packet.readInt8();
      const value = packet.readLengthCodedString('ascii');
      if (id === 0) {
        this.extendedTypeName = value; // e.g. 'uuid', 'inet4', 'inet6', 'point'
      } else if (id === 1) {
        this.extendedFormat = value; // e.g. 'json'
      }
    }
    // a malformed entry could read past the block; resynchronise so the
    // fixed column definition fields that follow parse from the right offset
    packet.offset = extendedMetadataEnd;
  }

  inspect() {
    return {
      catalog: this.catalog,
      schema: this.schema,
      name: this.name,
      orgName: this.orgName,
      table: this.table,
      orgTable: this.orgTable,
      characterSet: this.characterSet,
      encoding: this.encoding,
      columnLength: this.columnLength,
      type: this.columnType,
      flags: this.flags,
      decimals: this.decimals,
    };
  }

  [Symbol.for('nodejs.util.inspect.custom')](depth, inspectOptions, inspect) {
    const Types = require('../constants/types.js');
    const typeNames = [];
    for (const t in Types) {
      typeNames[Types[t]] = t;
    }
    const fiedFlags = require('../constants/field_flags.js');
    const flagNames = [];
    // TODO: respect inspectOptions.showHidden
    //const inspectFlags = inspectOptions.showHidden ? this.flags : this.flags & ~fiedFlags.PRI_KEY;
    const inspectFlags = this.flags;
    for (const f in fiedFlags) {
      if (inspectFlags & fiedFlags[f]) {
        if (f === 'PRI_KEY') {
          flagNames.push('PRIMARY KEY');
        } else if (f === 'NOT_NULL') {
          flagNames.push('NOT NULL');
        } else if (f === 'BINARY') {
          // ignore flag for now
        } else if (f === 'MULTIPLE_KEY') {
          // not sure if that should be part of inspection.
          // in the schema usually this is part of index definition
          // example: UNIQUE KEY `my_uniq_id` (`id_box_elements`,`id_router`)
          // note that only first column has MULTIPLE_KEY flag set in this case
          // so there is no good way of knowing that this is part of index just
          // by looking at indifidual field flags
        } else if (f === 'NO_DEFAULT_VALUE') {
          // almost the same as NOT_NULL?
        } else if (f === 'BLOB') {
          // included in the type
        } else if (f === 'UNSIGNED') {
          // this should be first after type
        } else if (f === 'TIMESTAMP') {
          // timestamp flag is redundant for inspection - already included in type
        } else if (f === 'ON_UPDATE_NOW') {
          flagNames.push('ON UPDATE CURRENT_TIMESTAMP');
        } else {
          flagNames.push(f);
        }
      }
    }

    if (depth > 1) {
      return inspect({
        ...this.inspect(),
        typeName: typeNames[this.columnType],
        flags: flagNames,
      });
    }

    const isUnsigned = this.flags & fiedFlags.UNSIGNED;

    let typeName = typeNames[this.columnType];
    if (typeName === 'BLOB') {
      // TODO: check for non-utf8mb4 encoding
      if (this.columnLength === 4294967295) {
        typeName = 'LONGTEXT';
      } else if (this.columnLength === 67108860) {
        typeName = 'MEDIUMTEXT';
      } else if (this.columnLength === 262140) {
        typeName = 'TEXT';
      } else if (this.columnLength === 1020) {
        // 255*4
        typeName = 'TINYTEXT';
      } else {
        typeName = `BLOB(${this.columnLength})`;
      }
    } else if (typeName === 'VAR_STRING') {
      // TODO: check for non-utf8mb4 encoding
      typeName = `VARCHAR(${Math.ceil(this.columnLength / 4)})`;
    } else if (typeName === 'TINY') {
      if (
        (this.columnLength === 3 && isUnsigned) ||
        (this.columnLength === 4 && !isUnsigned)
      ) {
        typeName = 'TINYINT';
      } else {
        typeName = `TINYINT(${this.columnLength})`;
      }
    } else if (typeName === 'LONGLONG') {
      if (this.columnLength === 20) {
        typeName = 'BIGINT';
      } else {
        typeName = `BIGINT(${this.columnLength})`;
      }
    } else if (typeName === 'SHORT') {
      if (isUnsigned && this.columnLength === 5) {
        typeName = 'SMALLINT';
      } else if (!isUnsigned && this.columnLength === 6) {
        typeName = 'SMALLINT';
      } else {
        typeName = `SMALLINT(${this.columnLength})`;
      }
    } else if (typeName === 'LONG') {
      if (isUnsigned && this.columnLength === 10) {
        typeName = 'INT';
      } else if (!isUnsigned && this.columnLength === 11) {
        typeName = 'INT';
      } else {
        typeName = `INT(${this.columnLength})`;
      }
    } else if (typeName === 'INT24') {
      if (isUnsigned && this.columnLength === 8) {
        typeName = 'MEDIUMINT';
      } else if (!isUnsigned && this.columnLength === 9) {
        typeName = 'MEDIUMINT';
      } else {
        typeName = `MEDIUMINT(${this.columnLength})`;
      }
    } else if (typeName === 'DOUBLE') {
      // DOUBLE without modifiers is reported as DOUBLE(22, 31)
      if (this.columnLength === 22 && this.decimals === 31) {
        typeName = 'DOUBLE';
      } else {
        typeName = `DOUBLE(${this.columnLength},${this.decimals})`;
      }
    } else if (typeName === 'FLOAT') {
      // FLOAT without modifiers is reported as FLOAT(12, 31)
      if (this.columnLength === 12 && this.decimals === 31) {
        typeName = 'FLOAT';
      } else {
        typeName = `FLOAT(${this.columnLength},${this.decimals})`;
      }
    } else if (typeName === 'NEWDECIMAL') {
      if (this.columnLength === 11 && this.decimals === 0) {
        typeName = 'DECIMAL';
      } else if (this.decimals === 0) {
        // not sure why, but DECIMAL(13) is reported as DECIMAL(14, 0)
        // and DECIMAL(13, 9) is reported as NEWDECIMAL(15, 9)
        if (isUnsigned) {
          typeName = `DECIMAL(${this.columnLength})`;
        } else {
          typeName = `DECIMAL(${this.columnLength - 1})`;
        }
      } else {
        typeName = `DECIMAL(${this.columnLength - 2},${this.decimals})`;
      }
    } else {
      typeName = `${typeNames[this.columnType]}(${this.columnLength})`;
    }

    if (isUnsigned) {
      typeName += ' UNSIGNED';
    }

    // TODO respect colors option
    return `\`${this.name}\` ${[typeName, ...flagNames].join(' ')}`;
  }

  static toPacket(column, sequenceId) {
    let length = 17; // = 4 padding + 1 + 12 for the rest
    fields.forEach((field) => {
      length += Packet.lengthCodedStringLength(
        column[field],
        CharsetToEncoding[column.characterSet]
      );
    });
    const buffer = Buffer.allocUnsafe(length);

    const packet = new Packet(sequenceId, buffer, 0, length);
    function writeField(name) {
      packet.writeLengthCodedString(
        column[name],
        CharsetToEncoding[column.characterSet]
      );
    }
    packet.offset = 4;
    fields.forEach(writeField);
    packet.writeInt8(0x0c);
    packet.writeInt16(column.characterSet);
    packet.writeInt32(column.columnLength);
    packet.writeInt8(column.columnType);
    packet.writeInt16(column.flags);
    packet.writeInt8(column.decimals);
    packet.writeInt16(0); // filler
    return packet;
  }

  // node-mysql compatibility: alias "db" to "schema"
  get db() {
    return this.schema;
  }
}

const addString = function (name) {
  const cacheKey = `_${name}Value`;
  const startKey = `_${name}Start`;
  const lengthKey = `_${name}Length`;
  ColumnDefinition.prototype[cacheKey] = undefined;
  Object.defineProperty(ColumnDefinition.prototype, name, {
    get: function () {
      const cached = this[cacheKey];
      if (cached !== undefined) {
        return cached;
      }
      const start = this[startKey];
      const end = start + this[lengthKey];
      const val = StringParser.decode(
        this._buf,
        this.encoding === 'binary' ? this._clientEncoding : this.encoding,
        start,
        end
      );
      this[cacheKey] = val;
      return val;
    },
  });
};

addString('catalog');
addString('schema');
addString('table');
addString('orgTable');
addString('orgName');

// MariaDB extended metadata defaults; instances only carry own values when
// the server actually tagged the column
ColumnDefinition.prototype.extendedTypeName = undefined;
ColumnDefinition.prototype.extendedFormat = undefined;

module.exports = ColumnDefinition;
