import { channel_ping, channel_pong } from "../channel_names";
import { decode, encode } from "../encode_decode_message";
import { ConnectionState, RTMT, RTMT_WS_PING_INTERVAL, RTMT_WS_PING_INTERVAL_BUFFER_TIME, RtmtEventTypes } from "./rtmt";
import { TinyEmitter } from "tiny-emitter";

const MESSAGE_CHANNEL_PREFIX = "message_channel";

export class RTMTWS extends TinyEmitter implements RTMT {
  private webSocket?: WebSocket;
  private pingTimeout?: number = undefined;
  private _connectionState: ConnectionState = "none";
  private userKey: string = "";
  private url: string = "";

  get connectionState(): ConnectionState {
    return this._connectionState;
  }

  protected setConnectionState(connectionState: ConnectionState) {
    this._connectionState = connectionState;
    this.emit("connectionchange", this._connectionState);
  }

  private createWebSocket(url: string) {
    const webSocket = new WebSocket(url);
    webSocket.onopen = () => this.onWebSocketOpen(webSocket);
    webSocket.onclose = () => this.onWebSocketClose(webSocket);
    webSocket.onmessage = (event: any) => this.onWebSocketMessage(webSocket, event);
    webSocket.onerror = (error: any) => this.onWebSocketError(webSocket, error);
  }

  private disposeWebSocket() {
    if (!this.webSocket) return;
    this.webSocket.onopen = () => { };
    this.webSocket.onclose = () => { };
    this.webSocket.onmessage = (event: any) => { };
    this.webSocket.onerror = (error: any) => { };
    this.webSocket = undefined;
  }

  public connectWebSocket(url: string, userKey: string) {
    this.setConnectionState("connecting");
    this.userKey = userKey;
    this.url = url;
    this.createWebSocket(url);
  }

  private reconnectWebSocket() {
    this.setConnectionState("reconnecting");
    this.disposeWebSocket();
    setTimeout(() => this.createWebSocket(this.url), 1000);
  }

  protected onWebSocketOpen(webSocket: WebSocket) {
    this.webSocket = webSocket;
    this.setConnectionState("connected");
    console.info("(RTMT) WebSocket has opened");
    this.heartbeat();
    this.emit("open");
  }

  protected onWebSocketMessage(webSocket: WebSocket, event: any) {
    const { channel, message } = decode(event.data);
    this.onMessage(channel, message);
  }

  protected onWebSocketError(webSocket: WebSocket, error: any) {
    this.setConnectionState("error");
    console.error(error);
    this.emit("error", error);
  }

  protected onWebSocketClose(webSocket: WebSocket) {
    this.setConnectionState("closed");
    console.info("(RTMT) WebSocket has closed");
    //this.WebSocket = undefined
    clearTimeout(this.pingTimeout);
    this.emit("close");
    this.reconnectWebSocket();
  }

  private heartbeat() {
    clearTimeout(this.pingTimeout);
    // @ts-ignore
    this.pingTimeout = setTimeout(() => {
      if (!this.webSocket) return;
      console.log("(RTMT) WebSocket self closed");
      this.webSocket.close();
      this.onWebSocketClose(this.webSocket);
    }, RTMT_WS_PING_INTERVAL + RTMT_WS_PING_INTERVAL_BUFFER_TIME);
  }

  public sendMessage(channel: string, message: Object) {
    if (this.webSocket === undefined) {
      console.error("(RTMT) WebSocket is undefined");
      return;
    }
    const data = encode({channel, message});
    this.webSocket.send(data);
  }

  private onMessage(channel: string, message: Object) {
    if (channel === channel_ping) {
      this.heartbeat();
      this.sendMessage(channel_pong, {});
      return;
    }
    // TODO: Maybe we should warn if there is not any listener for channel
    this.emit(MESSAGE_CHANNEL_PREFIX + channel, message);
  }

  public on(event: RtmtEventTypes, callback: (...value: any[]) => void): this {
    return super.on(event, callback);
  }

  public off(event: RtmtEventTypes, callback: (...value: any[]) => void): this {
    return super.off(event, callback);
  }

  public addMessageListener(channel: string, callback: (message: any) => void) {
    super.on(MESSAGE_CHANNEL_PREFIX + channel, callback);
  }

  public removeMessageListener(channel: string, callback: (message: any) => void) {
    super.off(MESSAGE_CHANNEL_PREFIX + channel, callback);
  }

  public dispose() {
    this.disposeWebSocket();
    // TODO
    //this.removeAllListeners();
  }
}
