'use strict';

const MAX_VARINT_BYTES = 5;
const MAX_STRING_BYTES = 32767 * 3;

function encodeVarInt(value) {
  if (value < 0) throw new RangeError('VarInt cannot be negative');
  const v = value >>> 0;
  if (v < 0x80) return Buffer.from([v]);
  const bytes = [];
  let n = v;
  while (n > 0x7F) {
    bytes.push((n & 0x7F) | 0x80);
    n >>>= 7;
  }
  bytes.push(n & 0x7F);
  return Buffer.from(bytes);
}

function parseVarInt(buf, offset = 0) {
  let value = 0;
  let shift = 0;
  let bytesRead = 0;
  if (offset < 0 || offset >= buf.length) return { value: 0, bytesRead: 0 };
  for (let i = offset; i < buf.length && i < offset + MAX_VARINT_BYTES; i++) {
    const byte = buf[i];
    value |= (byte & 0x7F) << shift;
    bytesRead++;
    if ((byte & 0x80) === 0) {
      return { value: value >>> 0, bytesRead };
    }
    shift += 7;
  }
  return { value: 0, bytesRead: 0 };
}

function encodeString(str) {
  const buf = Buffer.from(str, 'utf8');
  if (buf.length > MAX_STRING_BYTES) throw new RangeError('String too long');
  return Buffer.concat([encodeVarInt(buf.length), buf]);
}

function readString(buf, offset = 0) {
  const { value: len, bytesRead: lenBytes } = parseVarInt(buf, offset);
  if (lenBytes === 0) return { value: '', bytesRead: 0 };
  if (len < 0 || len > MAX_STRING_BYTES) return { value: '', bytesRead: 0 };
  const start = offset + lenBytes;
  if (start + len > buf.length) return { value: '', bytesRead: 0 };
  return {
    value: buf.slice(start, start + len).toString('utf8'),
    bytesRead: lenBytes + len,
  };
}

function buildPacket(packetId, data = Buffer.alloc(0)) {
  return Buffer.concat([encodeVarInt(packetId), data]);
}

function buildMCFrame(packetId, payload = Buffer.alloc(0)) {
  const inner = Buffer.concat([encodeVarInt(packetId), payload]);
  return Buffer.concat([encodeVarInt(inner.length), inner]);
}

function parseMCFrame(buf, offset = 0) {
  const start = offset;
  const { value: pktLen, bytesRead: lenBytes } = parseVarInt(buf, offset);
  if (lenBytes === 0) return { packetId: 0, data: Buffer.alloc(0), bytesRead: 0, complete: false };
  if (start + lenBytes + pktLen > buf.length) return { packetId: 0, data: Buffer.alloc(0), bytesRead: 0, complete: false };
  let innerOff = start + lenBytes;
  const { value: packetId, bytesRead: idBytes } = parseVarInt(buf, innerOff);
  innerOff += idBytes;
  const payloadLen = pktLen - idBytes;
  const data = buf.slice(innerOff, innerOff + payloadLen);
  return {
    packetId,
    data,
    bytesRead: lenBytes + pktLen,
    complete: true,
  };
}

function drainMCFrames(buf, onPacket) {
  let off = 0;
  let frames = 0;
  while (off < buf.length) {
    const frame = parseMCFrame(buf, off);
    if (!frame.complete) break;
    onPacket(frame.packetId, frame.data);
    off += frame.bytesRead;
    frames++;
  }
  return { consumed: off, frames };
}

module.exports = {
  encodeVarInt,
  parseVarInt,
  encodeString,
  readString,
  buildPacket,
  buildMCFrame,
  parseMCFrame,
  drainMCFrames,
  MAX_VARINT_BYTES,
  MAX_STRING_BYTES,
};
