import { EventHandler } from "./event-handler.js";

export class Socket extends EventHandler {
  /**
   * @type {URL}
   */
  url;
  /**
   * @type {WebSocket|null}
   */
  ws = null;

  /**
   * @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
   */
  connection = null;

  shouldReconnect = true;

  _debug = false;

  get isConnected() {
    return this.ws && this.ws.readyState === WebSocket.OPEN;
  }

  get isConnecting() {
    return this.ws && this.ws.readyState === WebSocket.CONNECTING;
  }

  constructor() {
    super();

    this.url = new URL("/rpc.ws", window.location.origin);
    this.url.protocol = this.url.protocol.replace("http", "ws");

    this.connect();
  }

  connect() {
    if (this.ws) {
      return this.connection.promise;
    }

    if (!this.connection || this.connection.resolved) {
      this.connection = Promise.withResolvers();
    }

    this.ws = new WebSocket(this.url);
    this.ws.addEventListener("open", () => {
      this.connection.resolved = true;
      this.connection.resolve(this);
      this.emit("connected");
    });

    this.ws.addEventListener("close", () => {
      console.log("Connection closed");
      this.disconnect();
    });
    this.ws.addEventListener("error", (e) => {
      console.error("Connection error", e);
      this.disconnect();
    });
    this.ws.addEventListener("message", (e) => {
      if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
        console.error("Binary data not supported");
      } else {
        try {
          this.onData(JSON.parse(e.data));
        } catch (e) {
          console.error("Failed to parse message", e);
        }
      }
    });
  }

  onData(data) {
    if (data.success !== undefined && !data.success) {
      console.error(data);
    }
    if (data.callId) {
      this.emit(data.callId, data.data);
    }
    if (data.channel_uid) {
      this.emit(data.channel_uid, data.data);
      if (!data["event"]) this.emit("channel-message", data);
    }
    this.emit("data", data.data);
    if (data["event"]) {
      this.emit(data.event, data.data);
    }
  }

  disconnect() {
    this.ws?.close();
    this.ws = null;

    if (this.shouldReconnect)
      setTimeout(() => {
        console.log("Reconnecting");
        this.emit("reconnecting");
          return this.connect();
      }, 0);
  }

  _camelToSnake(str) {
    return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
  }

  get client() {
    const me = this;
    return new Proxy(
      {},
      {
        get(_, prop) {
          return (...args) => {
            const functionName = me._camelToSnake(prop);
              if(me._debug){
                  const call = {}
                  call[functionName] = args 
                console.debug(call)
              }
              return me.call(functionName, ...args);
          };
        },
      },
    );
  }

  generateCallId() {
    return self.crypto.randomUUID();
  }

  async sendJson(data) {
    await this.connect().then((api) => {
      api.ws.send(JSON.stringify(data));
    });
  }

  async call(method, ...args) {
    const call = {
      callId: this.generateCallId(),
      method,
      args,
    };
    const me = this;
    return new Promise((resolve) => {
      me.addEventListener(call.callId, (data) => resolve(data));
      me.sendJson(call);
    });
  }
}
