import { Singleton, is_node, sleep_until } from "tstl";

import { Communicator } from "../../components/Communicator";
import { Invoke } from "../../components/Invoke";
import { IHeaderWrapper } from "../internal/IHeaderWrapper";
import { IServer } from "../internal/IServer";
import { once } from "../internal/once";
import { IWorkerSystem } from "./internal/IWorkerSystem";
import { ProcessChannel } from "./internal/processes/ProcessChannel";
import { ThreadPort } from "./internal/threads/ThreadPort";

/**
 * Worker Server.
 *
 * The `WorkerServer` is a class representing a `Worker` server which communicate
 * with client ({@link WorkerConnector}), through the RPC (Remote Procedure Call).
 *
 * Unlike other servers, `WorkerServer` can accept only one client
 * ({@link WorkerConnector}), because the `Worker` is dependent on its parent instance
 * (web page, node or parent worker). Thus, `WorkerServer` does not have any acceptor
 * and communicates with client (its parent) directly.
 *
 * To start communication with the client, call the {@link open} method
 * with `Provider` instance. After your business, don't forget {@link close closing}
 * this `Worker` instance. If the termination is performed by the
 * {@link WorkerConnector}, you can wait the closing signal through the
 * {@link join} method.
 *
 * Also, when declaring this `WorkerServer` 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 WorkerConnector} class
 * (`Provider` and `Remote` must be reversed).
 *
 * For reference, the first `Header` type represents an initial data from the
 * client after the connection. I recommend utilize it as an activation tool
 * for security enhancement. The second generic argument `Provider` represents a
 * provider from server to client, and the other `Remote` means a provider from the
 * client to server.
 *
 * @template Header Type of the header containing initial data.
 * @template Provider Type of features provided for the client.
 * @template Remote Type of features supported by client.
 * @author Jeongho Nam - https://github.com/samchon
 */
export class WorkerServer<
    Header,
    Provider extends object | null,
    Remote extends object | null,
  >
  extends Communicator<Provider | undefined, Remote>
  implements IWorkerSystem, IServer<WorkerServer.State>
{
  /**
   * @hidden
   */
  private channel_: Singleton<Promise<IFeature>>;

  /**
   * @hidden
   */
  private state_: WorkerServer.State;

  /**
   * @hidden
   */
  private header_: Singleton<Promise<Header>>;

  /* ----------------------------------------------------------------
    CONSTRUCTOR
  ---------------------------------------------------------------- */
  /**
   * Default Constructor.
   *
   * @param type You can specify the worker mode when NodeJS. Default is "thread".
   */
  public constructor() {
    super(undefined);
    this.channel_ = new Singleton(async () => {
      // BROWSER CASE
      if (is_node() === false) return (<any>self) as IFeature;

      return (await ThreadPort.isWorkerThread())
        ? ((await ThreadPort()) as IFeature)
        : (ProcessChannel as IFeature);
    });
    this.state_ = WorkerServer.State.NONE;
    this.header_ = new Singleton(async () => {
      (await this.channel_.get()).postMessage(WorkerServer.State.OPENING);

      const data: string = await this._Handshake("getHeader");
      const wrapper: IHeaderWrapper<Header> = JSON.parse(data);
      return wrapper.header;
    });
  }

  /**
   * Open server with `Provider`.
   *
   * Open worker server and start communication with the client
   * ({@link WorkerConnector}).
   *
   * Note that, after your business, you should terminate this worker to prevent
   * waste of memory leak. Close this worker by yourself ({@link close}) or let
   * client to close this worker ({@link WorkerConnector.close}).
   *
   * @param provider An object providing features for the client.
   */
  public async open(provider: Provider): Promise<void> {
    // TEST CONDITION
    if (is_node() === false) {
      if (self.document !== undefined)
        throw new Error("Error on WorkerServer.open(): this is not Worker.");
    } else if ((await this.channel_.get()).is_worker_server() === false)
      throw new Error("Error on WorkerServer.open(): this is not Worker.");
    else if (this.state_ !== WorkerServer.State.NONE)
      throw new Error(
        "Error on WorkerServer.open(): the server has been opened yet.",
      );

    // OPEN WORKER
    this.state_ = WorkerServer.State.OPENING;
    this.provider_ = provider;

    // GET HEADERS
    await this.header_.get();

    // SUCCESS
    const channel = await this.channel_.get();
    channel.onmessage = (evt) => this._Handle_message(evt);
    channel.postMessage(WorkerServer.State.OPEN);

    this.state_ = WorkerServer.State.OPEN;
  }

  /**
   * @inheritDoc
   */
  public async close(): Promise<void> {
    // TEST CONDITION
    const error: Error | null = this.inspectReady();
    if (error) throw error;

    //----
    // CLOSE WORKER
    //----
    this.state_ = WorkerServer.State.CLOSING;
    {
      // HANDLERS
      await this.destructor();

      // DO CLOSE
      setTimeout(async () => {
        const channel = await this.channel_.get();
        channel.postMessage(WorkerServer.State.CLOSING);
        channel.close();
      });
    }
    this.state_ = WorkerServer.State.CLOSED;
  }

  /* ----------------------------------------------------------------
    ACCESSORS
  ---------------------------------------------------------------- */
  /**
   * @inheritDoc
   */
  public get state(): WorkerServer.State {
    return this.state_;
  }

  /**
   * Get header containing initialization data like activation.
   */
  public getHeader(): Promise<Header> {
    return this.header_.get();
  }

  /**
   * @hidden
   */
  private _Handshake(
    method: string,
    timeout?: number,
    until?: Date,
  ): Promise<any> {
    return new Promise(async (resolve, reject) => {
      /* eslint-disable */
      let completed: boolean = false;

      /* eslint-disable */
      let expired: boolean = false;

      if (until !== undefined)
        sleep_until(until)
          .then(() => {
            if (completed === false) {
              reject(
                new Error(
                  `Error on WorkerConnector.${method}(): target worker is not sending handshake data over ${timeout} milliseconds.`,
                ),
              );
              expired = true;
            }
          })
          .catch(() => {});

      (await this.channel_.get()).onmessage = once((evt) => {
        if (expired === false) {
          completed = true;
          resolve(evt.data);
        }
      });
    });
  }

  /* ----------------------------------------------------------------
    COMMUNICATOR
  ---------------------------------------------------------------- */
  /**
   * @hidden
   */
  protected async sendData(invoke: Invoke): Promise<void> {
    (await this.channel_.get()).postMessage(JSON.stringify(invoke));
  }

  /**
   * @hidden
   */
  protected inspectReady(): Error | null {
    // NO ERROR
    if (this.state_ === WorkerServer.State.OPEN) return null;
    // ERROR, ONE OF THEM
    else if (this.state_ === WorkerServer.State.NONE)
      return new Error(
        "Error on WorkerServer.inspectReady(): server is not opened yet.",
      );
    else if (this.state_ === WorkerServer.State.OPENING)
      return new Error(
        "Error on WorkerServer.inspectReady(): server is on opening, wait for a sec.",
      );
    else if (this.state_ === WorkerServer.State.CLOSING)
      return new Error(
        "Error on WorkerServer.inspectReady(): server is on closing.",
      );
    // MAY NOT BE OCCURRED
    else if (this.state_ === WorkerServer.State.CLOSED)
      return new Error(
        "Error on WorkerServer.inspectReady(): the server has been closed.",
      );
    else
      return new Error(
        "Error on WorkerServer.inspectReady(): unknown error, but not connected.",
      );
  }

  /**
   * @hidden
   */
  private _Handle_message(evt: MessageEvent): void {
    if (evt.data === WorkerServer.State.CLOSING) this.close();
    else this.replyData(JSON.parse(evt.data));
  }
}

/**
 *
 */
export namespace WorkerServer {
  /**
   * Current state of the {@link WorkerServer}.
   */
  export import State = IServer.State;
}

//----
// POLYFILL
//----
/**
 * @hidden
 */
interface IFeature {
  close(): void;
  postMessage(message: any): void;
  onmessage(event: MessageEvent): void;
  is_worker_server(): boolean;
}
