'use strict';
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    }
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var stream = require('stream');
var PerMessageDeflate = require('./permessage-deflate');
var bufferUtil = require('./buffer-util');
var validation = require('./validation');
var constants = require('./constants');
var GET_INFO = 0;
var GET_PAYLOAD_LENGTH_16 = 1;
var GET_PAYLOAD_LENGTH_64 = 2;
var GET_MASK = 3;
var GET_DATA = 4;
var INFLATING = 5;
/**
 * HyBi Receiver implementation.
 *
 * @extends stream.Writable
 */
var Receiver = /** @class */ (function (_super) {
    __extends(Receiver, _super);
    /**
     * Creates a Receiver instance.
     *
     * @param {String} binaryType The type for binary data
     * @param {Object} extensions An object containing the negotiated extensions
     * @param {Number} maxPayload The maximum allowed message length
     */
    function Receiver(binaryType, extensions, maxPayload) {
        var _this = _super.call(this) || this;
        _this._binaryType = binaryType || constants.BINARY_TYPES[0];
        _this[constants.kWebSocket] = undefined;
        _this._extensions = extensions || {};
        _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;
        return _this;
    }
    /**
     * 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
     */
    Receiver.prototype._write = function (chunk, encoding, cb) {
        if (this._opcode === 0x08)
            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
     */
    Receiver.prototype.consume = function (n) {
        this._bufferedBytes -= n;
        if (n === this._buffers[0].length)
            return this._buffers.shift();
        if (n < this._buffers[0].length) {
            var buf = this._buffers[0];
            this._buffers[0] = buf.slice(n);
            return buf.slice(0, n);
        }
        var dst = Buffer.allocUnsafe(n);
        do {
            var buf = this._buffers[0];
            if (n >= buf.length) {
                this._buffers.shift().copy(dst, dst.length - n);
            }
            else {
                buf.copy(dst, dst.length - n, 0, n);
                this._buffers[0] = buf.slice(n);
            }
            n -= buf.length;
        } while (n > 0);
        return dst;
    };
    /**
     * Starts the parsing loop.
     *
     * @param {Function} cb Callback
     * @private
     */
    Receiver.prototype.startLoop = function (cb) {
        var 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
     */
    Receiver.prototype.getInfo = function () {
        if (this._bufferedBytes < 2) {
            this._loop = false;
            return;
        }
        var buf = this.consume(2);
        if ((buf[0] & 0x30) !== 0x00) {
            this._loop = false;
            return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002);
        }
        var compressed = (buf[0] & 0x40) === 0x40;
        if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
            this._loop = false;
            return error(RangeError, 'RSV1 must be clear', true, 1002);
        }
        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);
            }
            if (!this._fragmented) {
                this._loop = false;
                return error(RangeError, 'invalid opcode 0', true, 1002);
            }
            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);
            }
            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);
            }
            if (compressed) {
                this._loop = false;
                return error(RangeError, 'RSV1 must be clear', true, 1002);
            }
            if (this._payloadLength > 0x7d) {
                this._loop = false;
                return error(RangeError, "invalid payload length " + this._payloadLength, true, 1002);
            }
        }
        else {
            this._loop = false;
            return error(RangeError, "invalid opcode " + this._opcode, true, 1002);
        }
        if (!this._fin && !this._fragmented)
            this._fragmented = this._opcode;
        this._masked = (buf[1] & 0x80) === 0x80;
        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
     */
    Receiver.prototype.getPayloadLength16 = function () {
        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
     */
    Receiver.prototype.getPayloadLength64 = function () {
        if (this._bufferedBytes < 8) {
            this._loop = false;
            return;
        }
        var buf = this.consume(8);
        var 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);
        }
        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
     */
    Receiver.prototype.haveLength = function () {
        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);
            }
        }
        if (this._masked)
            this._state = GET_MASK;
        else
            this._state = GET_DATA;
    };
    /**
     * Reads mask bytes.
     *
     * @private
     */
    Receiver.prototype.getMask = function () {
        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
     */
    Receiver.prototype.getData = function (cb) {
        var data = constants.EMPTY_BUFFER;
        if (this._payloadLength) {
            if (this._bufferedBytes < this._payloadLength) {
                this._loop = false;
                return;
            }
            data = this.consume(this._payloadLength);
            if (this._masked)
                bufferUtil.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
     */
    Receiver.prototype.decompress = function (data, cb) {
        var _this = this;
        var perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
        perMessageDeflate.decompress(data, this._fin, function (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));
                }
                _this._fragments.push(buf);
            }
            var er = _this.dataMessage();
            if (er)
                return cb(er);
            _this.startLoop(cb);
        });
    };
    /**
     * Handles a data message.
     *
     * @return {(Error|undefined)} A possible error
     * @private
     */
    Receiver.prototype.dataMessage = function () {
        if (this._fin) {
            var messageLength = this._messageLength;
            var fragments = this._fragments;
            this._totalPayloadLength = 0;
            this._messageLength = 0;
            this._fragmented = 0;
            this._fragments = [];
            if (this._opcode === 2) {
                var data;
                if (this._binaryType === 'nodebuffer') {
                    data = toBuffer(fragments, messageLength);
                }
                else if (this._binaryType === 'arraybuffer') {
                    data = toArrayBuffer(toBuffer(fragments, messageLength));
                }
                else {
                    data = fragments;
                }
                this.emit('message', data);
            }
            else {
                var buf = toBuffer(fragments, messageLength);
                if (!validation.isValidUTF8(buf)) {
                    this._loop = false;
                    return error(Error, 'invalid UTF-8 sequence', true, 1007);
                }
                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
     */
    Receiver.prototype.controlMessage = function (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);
            }
            else {
                var code = data.readUInt16BE(0);
                if (!validation.isValidStatusCode(code)) {
                    return error(RangeError, "invalid status code " + code, true, 1002);
                }
                var buf = data.slice(2);
                if (!validation.isValidUTF8(buf)) {
                    return error(Error, 'invalid UTF-8 sequence', true, 1007);
                }
                this.emit('conclude', code, buf.toString());
                this.end();
            }
            return;
        }
        if (this._opcode === 0x09)
            this.emit('ping', data);
        else
            this.emit('pong', data);
        this._state = GET_INFO;
    };
    return Receiver;
}(stream.Writable));
module.exports = Receiver;
/**
 * Builds an error object.
 *
 * @param {(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
 * @return {(Error|RangeError)} The error
 * @private
 */
function error(ErrorCtor, message, prefix, statusCode) {
    var err = new ErrorCtor(prefix ? "Invalid WebSocket frame: " + message : message);
    Error.captureStackTrace(err, error);
    err[constants.kStatusCode] = statusCode;
    return err;
}
/**
 * Makes a buffer from a list of fragments.
 *
 * @param {Buffer[]} fragments The list of fragments composing the message
 * @param {Number} messageLength The length of the message
 * @return {Buffer}
 * @private
 */
function toBuffer(fragments, messageLength) {
    if (fragments.length === 1)
        return fragments[0];
    if (fragments.length > 1)
        return bufferUtil.concat(fragments, messageLength);
    return constants.EMPTY_BUFFER;
}
/**
 * Converts a buffer to an `ArrayBuffer`.
 *
 * @param {Buffer} The buffer to convert
 * @return {ArrayBuffer} Converted buffer
 */
function toArrayBuffer(buf) {
    if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
        return buf.buffer;
    }
    return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
