'use strict';

const { maxUnsigned16Bit, opcodes } = require('./constants');

const BUFFER_SIZE = 8 * 1024;

/** @type {import('crypto')} */
let crypto;
let buffer = null;
let bufIdx = BUFFER_SIZE;

try {
  crypto = require('node:crypto');
  /* c8 ignore next 3 */
} catch {
  crypto = {
    // not full compatibility, but minimum.
    randomFillSync: function randomFillSync(buffer, _offset, _size) {
      for (let i = 0; i < buffer.length; ++i) {
        buffer[i] = (Math.random() * 255) | 0;
      }
      return buffer;
    },
  };
}

function generateMask() {
  if (bufIdx === BUFFER_SIZE) {
    bufIdx = 0;
    crypto.randomFillSync(
      (buffer ??= Buffer.allocUnsafeSlow(BUFFER_SIZE)),
      0,
      BUFFER_SIZE
    );
  }
  return [
    buffer[bufIdx++],
    buffer[bufIdx++],
    buffer[bufIdx++],
    buffer[bufIdx++],
  ];
}

class WebsocketFrameSend {
  /**
   * @param {Buffer|undefined} data
   */
  constructor(data) {
    this.frameData = data;
  }

  createFrame(opcode) {
    const frameData = this.frameData;
    const maskKey = generateMask();
    const bodyLength = frameData?.byteLength ?? 0;

    /** @type {number} */
    let payloadLength = bodyLength; // 0-125
    let offset = 6;

    if (bodyLength > maxUnsigned16Bit) {
      offset += 8; // payload length is next 8 bytes
      payloadLength = 127;
    } else if (bodyLength > 125) {
      offset += 2; // payload length is next 2 bytes
      payloadLength = 126;
    }

    const buffer = Buffer.allocUnsafe(bodyLength + offset);

    // Clear first 2 bytes, everything else is overwritten
    buffer[0] = buffer[1] = 0;
    buffer[0] |= 0x80; // FIN
    buffer[0] = (buffer[0] & 0xf0) + opcode; // opcode

    /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
    buffer[offset - 4] = maskKey[0];
    buffer[offset - 3] = maskKey[1];
    buffer[offset - 2] = maskKey[2];
    buffer[offset - 1] = maskKey[3];

    buffer[1] = payloadLength;

    if (payloadLength === 126) {
      buffer.writeUInt16BE(bodyLength, 2);
    } else if (payloadLength === 127) {
      // Clear extended payload length
      buffer[2] = buffer[3] = 0;
      buffer.writeUIntBE(bodyLength, 4, 6);
    }

    buffer[1] |= 0x80; // MASK

    // mask body
    for (let i = 0; i < bodyLength; ++i) {
      buffer[offset + i] = frameData[i] ^ maskKey[i & 3];
    }

    return buffer;
  }

  /**
   * @param {Uint8Array} buffer
   */
  static createFastTextFrame(buffer) {
    const maskKey = generateMask();

    const bodyLength = buffer.length;

    // mask body
    for (let i = 0; i < bodyLength; ++i) {
      buffer[i] ^= maskKey[i & 3];
    }

    let payloadLength = bodyLength;
    let offset = 6;

    if (bodyLength > maxUnsigned16Bit) {
      offset += 8; // payload length is next 8 bytes
      payloadLength = 127;
    } else if (bodyLength > 125) {
      offset += 2; // payload length is next 2 bytes
      payloadLength = 126;
    }
    const head = Buffer.allocUnsafeSlow(offset);

    head[0] = 0x80 /* FIN */ | opcodes.TEXT; /* opcode TEXT */
    head[1] = payloadLength | 0x80; /* MASK */
    head[offset - 4] = maskKey[0];
    head[offset - 3] = maskKey[1];
    head[offset - 2] = maskKey[2];
    head[offset - 1] = maskKey[3];

    if (payloadLength === 126) {
      head.writeUInt16BE(bodyLength, 2);
    } else if (payloadLength === 127) {
      head[2] = head[3] = 0;
      head.writeUIntBE(bodyLength, 4, 6);
    }

    return [head, buffer];
  }
}

module.exports = {
  WebsocketFrameSend,
};