'use strict';

const PacketNodeEncoded = require('./packet-node-encoded');
const PacketIconvEncoded = require('./packet-node-iconv');

const Utils = require('../misc/utils');

/**
 * MySQL packet parser
 * see : https://mariadb.com/kb/en/library/0-packet/
 */
class PacketInputStream {
  constructor(unexpectedPacket, receiveQueue, out, opts, info) {
    this.unexpectedPacket = unexpectedPacket;
    this.opts = opts;
    this.receiveQueue = receiveQueue;
    this.info = info;
    this.out = out;

    //in case packet is not complete
    this.header = Buffer.allocUnsafe(4);
    this.headerLen = 0;
    this.packetLen = null;
    this.remainingLen = null;

    this.parts = null;
    this.partsTotalLen = 0;
    this.changeEncoding(this.opts.collation);
    this.changeDebug(this.opts.logPackets, this.opts.debug);
    this.opts.on('collation', this.changeEncoding.bind(this));
    this.opts.on('debug', this.changeDebug.bind(this));
  }

  changeEncoding(collation) {
    this.encoding = collation.charset;
    this.packetConstructor = Buffer.isEncoding(this.encoding)
      ? PacketNodeEncoded
      : PacketIconvEncoded;
  }

  changeDebug(logPackets, debug) {
    this.logPackets = logPackets;
    this.debug = debug;
    this.receivePacket =
      this.logPackets || this.debug ? this.receivePacketDebug : this.receivePacketBasic;
  }

  receivePacketDebug(packet) {
    let cmd = this.currentCmd();

    if (packet) {
      const packetStr = Utils.log(this.opts, packet.buf, packet.pos, packet.end, this.header);
      if (this.opts.logPackets) {
        this.info.addPacket(
          '<== conn:' +
            (this.info.threadId ? this.info.threadId : -1) +
            ' ' +
            (cmd
              ? cmd.onPacketReceive
                ? cmd.constructor.name + '.' + cmd.onPacketReceive.name
                : cmd.constructor.name
              : 'no command') +
            ' (' +
            packet.pos +
            ',' +
            packet.end +
            '))\n' +
            packetStr
        );
      }
      if (this.opts.debug) {
        console.log(
          '<== conn:%d %s (%d,%d)\n%s',
          this.info.threadId ? this.info.threadId : -1,
          cmd
            ? cmd.onPacketReceive
              ? cmd.constructor.name + '.' + cmd.onPacketReceive.name
              : cmd.constructor.name
            : 'no command',
          packet.pos,
          packet.end,
          packetStr
        );
      }
    }

    if (!cmd) {
      this.unexpectedPacket(packet);
      return;
    }

    cmd.sequenceNo = this.header[3];
    cmd.onPacketReceive(packet, this.out, this.opts, this.info);
    if (!cmd.onPacketReceive) this.receiveQueue.shift();
  }

  receivePacketBasic(packet) {
    let cmd = this.currentCmd();
    if (!cmd) {
      this.unexpectedPacket(packet);
      return;
    }
    cmd.sequenceNo = this.header[3];
    cmd.onPacketReceive(packet, this.out, this.opts, this.info);
    if (!cmd.onPacketReceive) this.receiveQueue.shift();
  }

  resetHeader() {
    this.remainingLen = null;
    this.headerLen = 0;
  }

  currentCmd() {
    let cmd;
    while ((cmd = this.receiveQueue.peek())) {
      if (cmd.onPacketReceive) return cmd;
      this.receiveQueue.shift();
    }
    return null;
  }

  onData(chunk) {
    let pos = 0;
    let length;
    const chunkLen = chunk.length;

    do {
      //read header
      if (this.remainingLen) {
        length = this.remainingLen;
      } else if (this.headerLen === 0 && chunkLen - pos >= 4) {
        this.header[0] = chunk[pos];
        this.header[1] = chunk[pos + 1];
        this.header[2] = chunk[pos + 2];
        this.header[3] = chunk[pos + 3];
        pos += 4;
        this.headerLen = 4;
        this.packetLen = this.header[0] + (this.header[1] << 8) + (this.header[2] << 16);
        length = this.packetLen;
      } else {
        length = null;
        while (chunkLen - pos > 0) {
          this.header[this.headerLen++] = chunk[pos++];
          if (this.headerLen === 4) {
            this.packetLen = this.header[0] + (this.header[1] << 8) + (this.header[2] << 16);
            length = this.packetLen;
            break;
          }
        }
      }

      if (length) {
        if (chunkLen - pos >= length) {
          const buf = chunk.slice(pos, pos + length);
          pos += length;
          if (this.parts) {
            this.parts.push(buf);
            this.partsTotalLen += length;

            if (this.packetLen < 0xffffff) {
              let buf = Buffer.concat(this.parts, this.partsTotalLen);
              this.parts = null;
              const packet = new this.packetConstructor(buf, 0, this.partsTotalLen, this.encoding);
              this.receivePacket(packet);
            }
          } else {
            if (this.packetLen < 0xffffff) {
              const packet = new this.packetConstructor(buf, 0, length, this.encoding);
              this.receivePacket(packet);
            } else {
              this.parts = [buf];
              this.partsTotalLen = length;
            }
          }
          this.resetHeader();
        } else {
          const buf = chunk.slice(pos, chunkLen);
          if (!this.parts) {
            this.parts = [buf];
            this.partsTotalLen = chunkLen - pos;
          } else {
            this.parts.push(buf);
            this.partsTotalLen += chunkLen - pos;
          }
          this.remainingLen = length - (chunkLen - pos);
          return;
        }
      }
    } while (pos < chunkLen);
  }
}

module.exports = PacketInputStream;
