‘use strict’;

const { Writable } = require(‘stream’);

const PerMessageDeflate = require(‘./permessage-deflate’); const {

BINARY_TYPES,
EMPTY_BUFFER,
kStatusCode,
kWebSocket

} = require(‘./constants’); const { concat, toArrayBuffer, unmask } = require(‘./buffer-util’); const { isValidStatusCode, isValidUTF8 } = require(‘./validation’);

const GET_INFO = 0; const GET_PAYLOAD_LENGTH_16 = 1; const GET_PAYLOAD_LENGTH_64 = 2; const GET_MASK = 3; const GET_DATA = 4; const INFLATING = 5;

/**

* HyBi Receiver implementation.
*
* @extends Writable
*/

class Receiver extends Writable {

/**
 * Creates a Receiver instance.
 *
 * @param {String} [binaryType=nodebuffer] The type for binary data
 * @param {Object} [extensions] An object containing the negotiated extensions
 * @param {Boolean} [isServer=false] Specifies whether to operate in client or
 *     server mode
 * @param {Number} [maxPayload=0] The maximum allowed message length
 */
constructor(binaryType, extensions, isServer, maxPayload) {
  super();

  this._binaryType = binaryType || BINARY_TYPES[0];
  this[kWebSocket] = undefined;
  this._extensions = extensions || {};
  this._isServer = !!isServer;
  this._maxPayload = maxPayload | 0;

  this._bufferedBytes = 0;
  this._buffers = [];

  this._compressed = false;
  this._payloadLength = 0;
  this._mask = undefined;
  this._fragmented = 0;
  this._masked = false;
  this._fin = false;
  this._opcode = 0;

  this._totalPayloadLength = 0;
  this._messageLength = 0;
  this._fragments = [];

  this._state = GET_INFO;
  this._loop = false;
}

/**
 * Implements `Writable.prototype._write()`.
 *
 * @param {Buffer} chunk The chunk of data to write
 * @param {String} encoding The character encoding of `chunk`
 * @param {Function} cb Callback
 * @private
 */
_write(chunk, encoding, cb) {
  if (this._opcode === 0x08 && this._state == GET_INFO) return cb();

  this._bufferedBytes += chunk.length;
  this._buffers.push(chunk);
  this.startLoop(cb);
}

/**
 * Consumes `n` bytes from the buffered data.
 *
 * @param {Number} n The number of bytes to consume
 * @return {Buffer} The consumed bytes
 * @private
 */
consume(n) {
  this._bufferedBytes -= n;

  if (n === this._buffers[0].length) return this._buffers.shift();

  if (n < this._buffers[0].length) {
    const buf = this._buffers[0];
    this._buffers[0] = buf.slice(n);
    return buf.slice(0, n);
  }

  const dst = Buffer.allocUnsafe(n);

  do {
    const buf = this._buffers[0];
    const offset = dst.length - n;

    if (n >= buf.length) {
      dst.set(this._buffers.shift(), offset);
    } else {
      dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
      this._buffers[0] = buf.slice(n);
    }

    n -= buf.length;
  } while (n > 0);

  return dst;
}

/**
 * Starts the parsing loop.
 *
 * @param {Function} cb Callback
 * @private
 */
startLoop(cb) {
  let err;
  this._loop = true;

  do {
    switch (this._state) {
      case GET_INFO:
        err = this.getInfo();
        break;
      case GET_PAYLOAD_LENGTH_16:
        err = this.getPayloadLength16();
        break;
      case GET_PAYLOAD_LENGTH_64:
        err = this.getPayloadLength64();
        break;
      case GET_MASK:
        this.getMask();
        break;
      case GET_DATA:
        err = this.getData(cb);
        break;
      default:
        // `INFLATING`
        this._loop = false;
        return;
    }
  } while (this._loop);

  cb(err);
}

/**
 * Reads the first two bytes of a frame.
 *
 * @return {(RangeError|undefined)} A possible error
 * @private
 */
getInfo() {
  if (this._bufferedBytes < 2) {
    this._loop = false;
    return;
  }

  const buf = this.consume(2);

  if ((buf[0] & 0x30) !== 0x00) {
    this._loop = false;
    return error(
      RangeError,
      'RSV2 and RSV3 must be clear',
      true,
      1002,
      'WS_ERR_UNEXPECTED_RSV_2_3'
    );
  }

  const compressed = (buf[0] & 0x40) === 0x40;

  if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
    this._loop = false;
    return error(
      RangeError,
      'RSV1 must be clear',
      true,
      1002,
      'WS_ERR_UNEXPECTED_RSV_1'
    );
  }

  this._fin = (buf[0] & 0x80) === 0x80;
  this._opcode = buf[0] & 0x0f;
  this._payloadLength = buf[1] & 0x7f;

  if (this._opcode === 0x00) {
    if (compressed) {
      this._loop = false;
      return error(
        RangeError,
        'RSV1 must be clear',
        true,
        1002,
        'WS_ERR_UNEXPECTED_RSV_1'
      );
    }

    if (!this._fragmented) {
      this._loop = false;
      return error(
        RangeError,
        'invalid opcode 0',
        true,
        1002,
        'WS_ERR_INVALID_OPCODE'
      );
    }

    this._opcode = this._fragmented;
  } else if (this._opcode === 0x01 || this._opcode === 0x02) {
    if (this._fragmented) {
      this._loop = false;
      return error(
        RangeError,
        `invalid opcode ${this._opcode}`,
        true,
        1002,
        'WS_ERR_INVALID_OPCODE'
      );
    }

    this._compressed = compressed;
  } else if (this._opcode > 0x07 && this._opcode < 0x0b) {
    if (!this._fin) {
      this._loop = false;
      return error(
        RangeError,
        'FIN must be set',
        true,
        1002,
        'WS_ERR_EXPECTED_FIN'
      );
    }

    if (compressed) {
      this._loop = false;
      return error(
        RangeError,
        'RSV1 must be clear',
        true,
        1002,
        'WS_ERR_UNEXPECTED_RSV_1'
      );
    }

    if (this._payloadLength > 0x7d) {
      this._loop = false;
      return error(
        RangeError,
        `invalid payload length ${this._payloadLength}`,
        true,
        1002,
        'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
      );
    }
  } else {
    this._loop = false;
    return error(
      RangeError,
      `invalid opcode ${this._opcode}`,
      true,
      1002,
      'WS_ERR_INVALID_OPCODE'
    );
  }

  if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
  this._masked = (buf[1] & 0x80) === 0x80;

  if (this._isServer) {
    if (!this._masked) {
      this._loop = false;
      return error(
        RangeError,
        'MASK must be set',
        true,
        1002,
        'WS_ERR_EXPECTED_MASK'
      );
    }
  } else if (this._masked) {
    this._loop = false;
    return error(
      RangeError,
      'MASK must be clear',
      true,
      1002,
      'WS_ERR_UNEXPECTED_MASK'
    );
  }

  if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
  else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
  else return this.haveLength();
}

/**
 * Gets extended payload length (7+16).
 *
 * @return {(RangeError|undefined)} A possible error
 * @private
 */
getPayloadLength16() {
  if (this._bufferedBytes < 2) {
    this._loop = false;
    return;
  }

  this._payloadLength = this.consume(2).readUInt16BE(0);
  return this.haveLength();
}

/**
 * Gets extended payload length (7+64).
 *
 * @return {(RangeError|undefined)} A possible error
 * @private
 */
getPayloadLength64() {
  if (this._bufferedBytes < 8) {
    this._loop = false;
    return;
  }

  const buf = this.consume(8);
  const num = buf.readUInt32BE(0);

  //
  // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
  // if payload length is greater than this number.
  //
  if (num > Math.pow(2, 53 - 32) - 1) {
    this._loop = false;
    return error(
      RangeError,
      'Unsupported WebSocket frame: payload length > 2^53 - 1',
      false,
      1009,
      'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
    );
  }

  this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
  return this.haveLength();
}

/**
 * Payload length has been read.
 *
 * @return {(RangeError|undefined)} A possible error
 * @private
 */
haveLength() {
  if (this._payloadLength && this._opcode < 0x08) {
    this._totalPayloadLength += this._payloadLength;
    if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
      this._loop = false;
      return error(
        RangeError,
        'Max payload size exceeded',
        false,
        1009,
        'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
      );
    }
  }

  if (this._masked) this._state = GET_MASK;
  else this._state = GET_DATA;
}

/**
 * Reads mask bytes.
 *
 * @private
 */
getMask() {
  if (this._bufferedBytes < 4) {
    this._loop = false;
    return;
  }

  this._mask = this.consume(4);
  this._state = GET_DATA;
}

/**
 * Reads data bytes.
 *
 * @param {Function} cb Callback
 * @return {(Error|RangeError|undefined)} A possible error
 * @private
 */
getData(cb) {
  let data = EMPTY_BUFFER;

  if (this._payloadLength) {
    if (this._bufferedBytes < this._payloadLength) {
      this._loop = false;
      return;
    }

    data = this.consume(this._payloadLength);
    if (this._masked) unmask(data, this._mask);
  }

  if (this._opcode > 0x07) return this.controlMessage(data);

  if (this._compressed) {
    this._state = INFLATING;
    this.decompress(data, cb);
    return;
  }

  if (data.length) {
    //
    // This message is not compressed so its lenght is the sum of the payload
    // length of all fragments.
    //
    this._messageLength = this._totalPayloadLength;
    this._fragments.push(data);
  }

  return this.dataMessage();
}

/**
 * Decompresses data.
 *
 * @param {Buffer} data Compressed data
 * @param {Function} cb Callback
 * @private
 */
decompress(data, cb) {
  const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];

  perMessageDeflate.decompress(data, this._fin, (err, buf) => {
    if (err) return cb(err);

    if (buf.length) {
      this._messageLength += buf.length;
      if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
        return cb(
          error(
            RangeError,
            'Max payload size exceeded',
            false,
            1009,
            'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
          )
        );
      }

      this._fragments.push(buf);
    }

    const er = this.dataMessage();
    if (er) return cb(er);

    this.startLoop(cb);
  });
}

/**
 * Handles a data message.
 *
 * @return {(Error|undefined)} A possible error
 * @private
 */
dataMessage() {
  if (this._fin) {
    const messageLength = this._messageLength;
    const fragments = this._fragments;

    this._totalPayloadLength = 0;
    this._messageLength = 0;
    this._fragmented = 0;
    this._fragments = [];

    if (this._opcode === 2) {
      let data;

      if (this._binaryType === 'nodebuffer') {
        data = concat(fragments, messageLength);
      } else if (this._binaryType === 'arraybuffer') {
        data = toArrayBuffer(concat(fragments, messageLength));
      } else {
        data = fragments;
      }

      this.emit('message', data);
    } else {
      const buf = concat(fragments, messageLength);

      if (!isValidUTF8(buf)) {
        this._loop = false;
        return error(
          Error,
          'invalid UTF-8 sequence',
          true,
          1007,
          'WS_ERR_INVALID_UTF8'
        );
      }

      this.emit('message', buf.toString());
    }
  }

  this._state = GET_INFO;
}

/**
 * Handles a control message.
 *
 * @param {Buffer} data Data to handle
 * @return {(Error|RangeError|undefined)} A possible error
 * @private
 */
controlMessage(data) {
  if (this._opcode === 0x08) {
    this._loop = false;

    if (data.length === 0) {
      this.emit('conclude', 1005, '');
      this.end();
    } else if (data.length === 1) {
      return error(
        RangeError,
        'invalid payload length 1',
        true,
        1002,
        'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
      );
    } else {
      const code = data.readUInt16BE(0);

      if (!isValidStatusCode(code)) {
        return error(
          RangeError,
          `invalid status code ${code}`,
          true,
          1002,
          'WS_ERR_INVALID_CLOSE_CODE'
        );
      }

      const buf = data.slice(2);

      if (!isValidUTF8(buf)) {
        return error(
          Error,
          'invalid UTF-8 sequence',
          true,
          1007,
          'WS_ERR_INVALID_UTF8'
        );
      }

      this.emit('conclude', code, buf.toString());
      this.end();
    }
  } else if (this._opcode === 0x09) {
    this.emit('ping', data);
  } else {
    this.emit('pong', data);
  }

  this._state = GET_INFO;
}

}

module.exports = Receiver;

/**

* Builds an error object.
*
* @param {function(new:Error|RangeError)} ErrorCtor The error constructor
* @param {String} message The error message
* @param {Boolean} prefix Specifies whether or not to add a default prefix to
*     `message`
* @param {Number} statusCode The status code
* @param {String} errorCode The exposed error code
* @return {(Error|RangeError)} The error
* @private
*/

function error(ErrorCtor, message, prefix, statusCode, errorCode) {

const err = new ErrorCtor(
  prefix ? `Invalid WebSocket frame: ${message}` : message
);

Error.captureStackTrace(err, error);
err.code = errorCode;
err[kStatusCode] = statusCode;
return err;

}