import { EngineRequest, Transport } from "../transport";
import debugModule from "debug";
import type { Packet, RawData } from "engine.io-parser";
import type { PerMessageDeflateOptions, WebSocket as WsWebSocket } from "ws";

const debug = debugModule("engine:ws");

export class WebSocket extends Transport {
  perMessageDeflate?: boolean | PerMessageDeflateOptions;
  private socket: WsWebSocket;

  /**
   * WebSocket transport
   *
   * @param {EngineRequest} req
   */
  constructor(req: EngineRequest) {
    super(req);
    this.socket = req.websocket;
    this.socket.on("message", (data, isBinary) => {
      const message = isBinary ? data : data.toString();
      debug('received "%s"', message);
      super.onData(message);
    });
    this.socket.once("close", this.onClose.bind(this));
    this.socket.on("error", this.onError.bind(this));
    this.writable = true;
    this.perMessageDeflate = null;
  }

  /**
   * Transport name
   */
  get name() {
    return "websocket";
  }

  /**
   * Advertise upgrade support.
   */
  get handlesUpgrades() {
    return true;
  }

  send(packets: Packet[]) {
    this.writable = false;

    for (let i = 0; i < packets.length; i++) {
      const packet = packets[i];
      const isLast = i + 1 === packets.length;

      if (this._canSendPreEncodedFrame(packet)) {
        // the WebSocket frame was computed with WebSocket.Sender.frame()
        // see https://github.com/websockets/ws/issues/617#issuecomment-283002469
        // @ts-expect-error use of untyped member
        this.socket._sender.sendFrame(
          packet.options.wsPreEncodedFrame,
          isLast ? this._onSentLast : this._onSent,
        );
      } else {
        this.parser.encodePacket(
          packet,
          this.supportsBinary,
          isLast ? this._doSendLast : this._doSend,
        );
      }
    }
  }

  /**
   * Whether the encoding of the WebSocket frame can be skipped.
   * @param packet
   * @private
   */
  private _canSendPreEncodedFrame(packet: Packet) {
    return (
      !this.perMessageDeflate &&
      // @ts-expect-error use of untyped member
      typeof this.socket?._sender?.sendFrame === "function" &&
      packet.options?.wsPreEncodedFrame !== undefined
    );
  }

  private _doSend = (data: RawData) => {
    this.socket.send(data, this._onSent);
  };

  private _doSendLast = (data: RawData) => {
    this.socket.send(data, this._onSentLast);
  };

  private _onSent = (err?: Error) => {
    if (err) {
      this.onError("write error", err.stack);
    }
  };

  private _onSentLast = (err?: Error) => {
    if (err) {
      this.onError("write error", err.stack);
    } else {
      this.emit("drain");
      this.writable = true;
      this.emit("ready");
    }
  };

  doClose(fn?: () => void) {
    debug("closing");
    this.socket.close();
    fn && fn();
  }
}
