import { is_node, sleep_for } from "tstl";

import { Invoke } from "../../components/Invoke";
import { ConnectorBase } from "../internal/ConnectorBase";
import { IHeaderWrapper } from "../internal/IHeaderWrapper";
import { once } from "../internal/once";
import { WebSocketError } from "./WebSocketError";
import { IWebSocketCommunicator } from "./internal/IWebSocketCommunicator";
import { WebSocketPolyfill } from "./internal/WebSocketPolyfill";

/**
 * Web Socket Connector.
 *
 * The `WebSocketConnector` is a communicator class which connects to a
 * {@link WebSocketServer websocket server}, and interacts with it through RPC
 * (Remote Procedure Call) concept.
 *
 * You can connect to the websocket server using {@link connect} method. The
 * interaction would be started if the server accepts your connection by calling
 * the {@link WebSocketAcceptor.accept} method. If the remote server rejects your
 * connection through {@link WebSocketAcceptor.reject} method, the exception
 * would be thrown.
 *
 * After the connection, don't forget to {@link closing} the connection, if your
 * business logics have been completed, to clean up the resources. Otherwise, the
 * closing must be performed by the remote websocket server, you can wait the
 * remote server's closing signal through the {@link join} method.
 *
 * Also, when declaring this `WebSocketConnector` type, you've to define three
 * generic arguments; `Header`, `Provider` and `Remote`. Those generic arguments must
 * be same with the ones defined in the target {@link WebSocketServer} and
 * {@link WebSocketAcceptor} classes (`Provider` and `Remote` must be reversed).
 *
 * For reference, the first `Header` type represents an initial data from the
 * remote client after the connection. I recommend utilize it as an activation tool
 * for security enhancement. The second generic argument `Provider` represents a
 * provider from client to server, and the other `Remote` means a provider from the
 * remote server to client.
 *
 * @template Header Type of the header containing initial data.
 * @template Provider Type of features provided for the remote server.
 * @template Remote Type of features supported by remote server.
 * @author Jeongho Nam - https://github.com/samchon
 */
export class WebSocketConnector<
    Header,
    Provider extends object | null,
    Remote extends object | null,
  >
  extends ConnectorBase<Header, Provider, Remote>
  implements IWebSocketCommunicator
{
  /**
   * @hidden
   */
  private socket_?: WebSocket;

  /* ----------------------------------------------------------------
    CONNECTION
  ---------------------------------------------------------------- */
  /**
   * Connect to remote websocket server.
   *
   * Try connection to the remote websocket server with its address and waiting for the
   * server to accept the trial. If the server rejects your connection, then exception
   * would be thrown (in *Promise.catch*, as `WebSocketError`).
   *
   * After the connection and your business has been completed, don't forget to closing the
   * connection in time to prevent waste of the server resource.
   *
   * @param url URL address to connect.
   * @param options Detailed options like timeout.
   */
  public async connect(
    url: string,
    options: Partial<WebSocketConnector.IConnectOptions> = {},
  ): Promise<void> {
    // TEST CONDITION
    if (this.socket_ && this.state !== WebSocketConnector.State.CLOSED)
      if (this.socket_.readyState === WebSocketConnector.State.CONNECTING)
        throw new Error(
          "Error on WebSocketConnector.connect(): already connecting.",
        );
      else if (this.socket_.readyState === WebSocketConnector.State.OPEN)
        throw new Error(
          "Error on WebSocketConnector.connect(): already connected.",
        );
      else
        throw new Error(
          "Error on WebSocketConnector.connect(): already closing.",
        );

    //----
    // CONNECTION
    //----
    // PREPARE ASSETS
    this.state_ = WebSocketConnector.State.CONNECTING;

    try {
      // DO CONNECT
      const factory = is_node()
        ? ((await WebSocketPolyfill()) as any)
        : self.WebSocket;
      this.socket_ = new factory(url);
      await this._Wait_connection();

      // SEND HEADERS
      this.socket_!.send(JSON.stringify(IHeaderWrapper.wrap(this.header)));

      // PROMISED HANDSHAKE
      if (
        (await this._Handshake(options.timeout)) !==
        WebSocketConnector.State.OPEN.toString()
      )
        throw new WebSocketError(
          1008,
          "Error on WebSocketConnector.connect(): target server may not be opened by TGrid. It's not following the TGrid's own handshake rule.",
        );

      // SUCCESS
      this.state_ = WebSocketConnector.State.OPEN;
      {
        this.socket_!.onmessage = this._Handle_message.bind(this);
        this.socket_!.onclose = this._Handle_close.bind(this);
        this.socket_!.onerror = () => {};
      }
    } catch (exp) {
      this.state_ = WebSocketConnector.State.NONE;
      if (
        this.socket_ &&
        this.socket_.readyState === WebSocketConnector.State.OPEN
      ) {
        this.socket_.onclose = () => {};
        this.socket_.close();
      }
      throw exp;
    }
  }

  /**
   * @hidden
   */
  private _Wait_connection(): Promise<WebSocket> {
    return new Promise((resolve, reject) => {
      this.socket_!.onopen = () => resolve(this.socket_!);
      this.socket_!.onclose = once((evt) => {
        reject(new WebSocketError(evt.code, evt.reason));
      });
      this.socket_!.onerror = once((evt) => {
        reject(
          new WebSocketError(
            1006,
            `Error on WebSocketConnector.connect(): ${(evt as any as ErrorEvent)?.message ?? "connection refused."}`,
          ),
        );
      });
    });
  }

  /**
   * @inheritDoc
   */
  public async close(code?: number, reason?: string): Promise<void> {
    // TEST CONDITION
    const error: Error | null = this.inspectReady("close");
    if (error) throw error;

    //----
    // CLOSE WITH JOIN
    //----
    // PREPARE JOINER
    const ret: Promise<void> = this.join();

    // DO CLOSE
    this.state_ = WebSocketConnector.State.CLOSING;
    this.socket_!.close(code, reason);

    // LAZY RETURN
    await ret;
  }

  /**
   * @hidden
   */
  private _Handshake(timeout?: number): Promise<string> {
    return new Promise((resolve, reject) => {
      /* eslint-disable */
      let completed: boolean = false;

      /* eslint-disable */
      let expired: boolean = false;

      // TIMEOUT
      if (timeout !== undefined)
        sleep_for(timeout).then(() => {
          if (completed === false) {
            reject(
              new WebSocketError(
                1008,
                `Error on WebSocketConnector.connect(): target server is not sending handshake data over ${timeout} milliseconds.`,
              ),
            );
            expired = true;
          }
        });

      // EVENT LISTENERS
      this.socket_!.onmessage = once((evt) => {
        if (expired === false) {
          completed = true;
          resolve(evt.data);
        }
      });
      this.socket_!.onclose = once((evt) => {
        if (expired === false) {
          completed = true;
          reject(new WebSocketError(evt.code, evt.reason));
        }
      });
      this.socket_!.onerror = once(() => {
        if (expired === false) {
          completed = true;
          reject(
            new WebSocketError(
              1006,
              "Error on WebSocketConnector.connect(): connection refused.",
            ),
          );
        }
      });
    });
  }

  /* ----------------------------------------------------------------
    ACCESSORS
  ---------------------------------------------------------------- */
  /**
   * Connection URL.
   */
  public get url(): string | undefined {
    return this.socket_ ? this.socket_.url : undefined;
  }

  /**
   * Get state.
   *
   * Get current state of connection state with the websocket server.
   *
   * List of values are such like below:
   *
   *   - `NONE`: The {@link WebSocketConnector} instance is newly created, but did nothing yet.
   *   - `CONNECTING`: The {@link WebSocketConnector.connect} method is on running.
   *   - `OPEN`: The connection is online.
   *   - `CLOSING`: The {@link WebSocketConnector.close} method is on running.
   *   - `CLOSED`: The connection is offline.
   */
  public get state(): WebSocketConnector.State {
    return this.state_;
  }

  /* ----------------------------------------------------------------
    COMMUNICATOR
  ---------------------------------------------------------------- */
  /**
   * @hidden
   */
  protected async sendData(invoke: Invoke): Promise<void> {
    this.socket_!.send(JSON.stringify(invoke));
  }

  /**
   * @hidden
   */
  private _Handle_message(evt: MessageEvent): void {
    if (typeof evt.data === "string") {
      const invoke: Invoke = JSON.parse(evt.data);
      this.replyData(invoke);
    }
  }

  /**
   * @hidden
   */
  private async _Handle_close(event: CloseEvent): Promise<void> {
    const error: WebSocketError | undefined =
      !event.code || event.code !== 1000
        ? new WebSocketError(event.code, event.reason)
        : undefined;

    this.state_ = WebSocketConnector.State.CLOSED;
    await this.destructor(error);
  }
}

/**
 *
 */
export namespace WebSocketConnector {
  /**
   * Current state of the {@link WebSocketConnector}.
   */
  export import State = ConnectorBase.State;

  /**
   * Connection options for the {@link WebSocketConnector.connect}.
   */
  export interface IConnectOptions {
    /**
     * Milliseconds to wait the web-socket server to accept or reject it. If omitted, the waiting would be forever.
     */
    timeout: number;
  }
}
