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

'use strict';

import PacketNodeEncoded from './packet-node-encoded.js';
import PacketIconvEncoded from './packet-node-iconv.js';
import Collations from '../const/collations.js';
import { log } from '../misc/utils.js';

/**
 * 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._cmd = null;
    this.changeEncoding(this.opts.collation ? this.opts.collation : Collations.fromIndex(224));
    this.changeDebug(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.packet = Buffer.isEncoding(this.encoding)
      ? new PacketNodeEncoded(this.encoding)
      : new PacketIconvEncoded(this.encoding);
  }

  changeDebug(debug) {
    this._debug = debug;
    this.receivePacket = debug ? this.receivePacketDebug : this.receivePacketBasic;
  }

  receivePacketDebug(packet) {
    let cmd = this._cmd;
    if (!cmd || !cmd.onPacketReceive) {
      cmd = this.currentCmd();
    }
    this.header[0] = this.packetLen;
    this.header[1] = this.packetLen >> 8;
    this.header[2] = this.packetLen >> 16;
    this.header[3] = this.sequenceNo;
    if (packet) {
      this.opts.logger.network(
        `<== 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${log(this.opts, packet.buf, packet.pos, packet.end, this.header)}`
      );
    }

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

    cmd.sequenceNo = this.sequenceNo;
    cmd.onPacketReceive(packet, this.out, this.opts, this.info);
    if (!cmd.onPacketReceive) {
      this._cmd = null;
      this.receiveQueue.shift();
    }
  }

  receivePacketBasic(packet) {
    let cmd = this._cmd;
    if (!cmd || !cmd.onPacketReceive) {
      cmd = this.currentCmd();
      if (!cmd) {
        this.unexpectedPacket(packet);
        return;
      }
    }
    cmd.sequenceNo = this.sequenceNo;
    cmd.onPacketReceive(packet, this.out, this.opts, this.info);
    if (!cmd.onPacketReceive) {
      this._cmd = null;
      this.receiveQueue.shift();
    }
  }

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

  currentCmd() {
    let cmd;
    while ((cmd = this.receiveQueue.peek())) {
      if (cmd.onPacketReceive) {
        this._cmd = cmd;
        return cmd;
      }
      this.receiveQueue.shift();
    }
    this._cmd = null;
    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.packetLen = chunk[pos] + (chunk[pos + 1] << 8) + (chunk[pos + 2] << 16);
        this.sequenceNo = chunk[pos + 3];
        pos += 4;
        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);
            this.sequenceNo = this.header[3];
            length = this.packetLen;
            break;
          }
        }
      }

      if (length) {
        if (chunkLen - pos >= length) {
          pos += length;
          if (!this.parts) {
            if (this.packetLen < 0xffffff) {
              this.receivePacket(this.packet.update(chunk, pos - length, pos));
              // fast path, knowing there is no parts
              // loop can be simplified until reaching the end of the packet.
              // cache cmd + packet refs to avoid repeated lookups in tight row loop
              let cmd = this._cmd;
              const pkt = this.packet;
              while (pos + 4 < chunkLen) {
                this.packetLen = chunk[pos] + (chunk[pos + 1] << 8) + (chunk[pos + 2] << 16);
                this.sequenceNo = chunk[pos + 3];
                pos += 4;
                if (chunkLen - pos >= this.packetLen) {
                  pos += this.packetLen;
                  if (this.packetLen < 0xffffff) {
                    // row fast-path: if first byte < 0xfe, it's a data row — dispatch directly
                    // skip fast-path in debug mode so all packets get logged
                    if (!this._debug && cmd && cmd.onPacketReceive && chunk[pos - this.packetLen] < 0xfe) {
                      cmd.sequenceNo = this.sequenceNo;
                      cmd.onPacketReceive(pkt.update(chunk, pos - this.packetLen, pos), this.out, this.opts, this.info);
                    } else {
                      this.receivePacket(pkt.update(chunk, pos - this.packetLen, pos));
                      cmd = this._cmd;
                    }
                  } else {
                    this.parts = [chunk.subarray(pos - this.packetLen, pos)];
                    this.partsTotalLen = this.packetLen;
                    break;
                  }
                } else {
                  const buf = chunk.subarray(pos, chunkLen);
                  if (!this.parts) {
                    this.parts = [buf];
                    this.partsTotalLen = chunkLen - pos;
                  } else {
                    this.parts.push(buf);
                    this.partsTotalLen += chunkLen - pos;
                  }
                  this.remainingLen = this.packetLen - (chunkLen - pos);
                  return;
                }
              }
            } else {
              this.parts = [chunk.subarray(pos - length, pos)];
              this.partsTotalLen = length;
            }
          } else {
            this.parts.push(chunk.subarray(pos - length, pos));
            this.partsTotalLen += length;

            if (this.packetLen < 0xffffff) {
              let buf = Buffer.concat(this.parts, this.partsTotalLen);
              this.parts = null;
              this.receivePacket(this.packet.update(buf, 0, this.partsTotalLen));
            }
          }
          this.resetHeader();
        } else {
          const buf = chunk.subarray(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;
        }
      } else if (length === 0 && this.parts) {
        // ending empty packet
        this.parts.push(chunk.subarray(pos - length, pos));
        let buf = Buffer.concat(this.parts, this.partsTotalLen);
        this.receivePacket(this.packet.update(buf, 0, this.partsTotalLen));
        this.parts = null;
        this.resetHeader();
      }
    } while (pos < chunkLen);
  }
}

export default PacketInputStream;
