import { v4 } from "uuid";
import { readHello } from "./protocol/hello";
import { BinaryStreamReader } from "./binary/readerStream";
import { ReadKind, readEvent } from "./protocol/serverEvent";
import { AudioManager } from "./audio.svelte";
import { type WriteEvent, writeEvent, WriteKind } from "./protocol/clientEvent";
import { BinaryWriter } from "./binary/writer";

export class Client {
  static async connect(audioManager: AudioManager, outputAudio: HTMLDivElement) {
    const transport = new WebTransport(`https://${location.hostname}:4433`, { allowPooling: false });
    await transport.ready;
    console.log("ready!");
    const stream = await transport.createBidirectionalStream();

    const uuid = v4();
    const writer = stream.writable.getWriter();
    await writer.write(new TextEncoder().encode(uuid));
    const reader = BinaryStreamReader.fromReader(stream.readable.getReader());
    const { players, peers } = await readHello(reader);
    console.log("done reading hello");

    const playersBind = $state(players);
    const client = new Client(audioManager, outputAudio, playersBind, uuid, transport, stream, writer);

    for (const id of peers) {
      client.getPeer(id).offer();
    }

    // peer.on("call", (conn) => {
    //   conn.answer(audioManager.output);
    //   client.peers[conn.connectionId] = new Peer(conn.connectionId, outputAudio, conn)
    // });

    console.log("done setup, calling handlers");
    client.datagramEventHandler();
    client.streamEventHandler(reader);
    client.closeHandler();

    return client;
  }

  private async closeHandler() {
    await this.transport.closed;
    console.error("closed");
  }

  private async datagramEventHandler() {
    const dgramReader = this.transport.datagrams.readable.getReader();
    this.eventHandler(async () => {
      const dgram = await dgramReader.read();
      console.log(dgram);
      throw dgram;
    })
  }

  private getPeer(id: string) {
    if (this.peers[id] !== undefined) {
      return this.peers[id];
    }
    return this.peers[id] = new Peer(id, this);
  }

  async writeEvent(event: WriteEvent) {
    const binaryWriter = new BinaryWriter(0x100);
    binaryWriter.write(event, writeEvent);
    await this.writer.write(binaryWriter.toBytes());
  }

  private async streamEventHandler(reader: BinaryStreamReader) {
    this.eventHandler(() => readEvent(reader))
  }
  private async eventHandler(eventSource: () => ReturnType<typeof readEvent>) {
    while (true) {
      const event = await eventSource();
      console.log("got event", event);
      switch (event.kind) {
        case ReadKind.Connected: {
          console.log("hello", event.name);
          this.players[event.id] = new Player(event.name, event.id)
          break;
        }
        case ReadKind.Disconnected: {
          console.log("goodbye", event.id);
          delete this.players[event.id];
          break;
        }
        case ReadKind.Moved: {
          if (this.players[event.id] !== undefined)
            this.players[event.id].position = event.position;
          break;
        }
        case ReadKind.StageChanged: {
          console.log("goodbye", event.id);
          break;
        }
        case ReadKind.PeerConnectionChanged: {
          console.log("peer changed", event.id, event.connected);
          // if (event.connected) {
          //   this.getPeer(event.id)
          // }
          break;
        }
        case ReadKind.Offer: {
          this.getPeer(event.id).gotOffer(event.offerSdp);
          break;
        }
        case ReadKind.Answer: {
          this.getPeer(event.id).gotAnswer(event.answerSdp);
          break;
        }
        case ReadKind.IceCandidate: {
          this.getPeer(event.id).gotCandidate(event.candidate);
          break;
        }
      }
    }
  }

  private peers: Record<string, Peer> = $state({});

  private constructor(
    public audioManager: AudioManager,
    public outputAudio: HTMLDivElement,
    public players: Record<number, Player>,
    public uuid: string,
    public transport: WebTransport,
    public stream: WebTransportBidirectionalStream,
    public writer: WritableStreamDefaultWriter,
  ) {
  }

  closed() {
    return this.transport.closed;
  }
}

export class Player {
  constructor(public name: string, public id: number, public position: number[] = [0, 0, 0], public stage: string = "nostage" + Math.random()) { }
}

export class Peer {
  private connection: RTCPeerConnection;
  private audel: HTMLAudioElement;
  private state: string = $state("not connected");
  constructor(public peerId: string, public client: Client) {
    this.connection = new RTCPeerConnection({
      iceServers: [
        {
          urls: ["stun:stun.l.google.com:19302"],
        }
      ]
    });
    for (const track of this.client.audioManager.output.getAudioTracks()) {
      this.connection.addTrack(track, this.client.audioManager.output);
    }
    this.connection.addEventListener("iceconnectionstatechange", (state) => {
      console.log(this.connection.iceConnectionState);
      this.state = this.connection.iceConnectionState;
    });
    this.connection.addEventListener("icecandidate", (event) => {
      if (event.candidate)
        this.client.writeEvent({ kind: WriteKind.IceCandidate, to: this.peerId, candidate: event.candidate });
      else
        console.log(this.peerId, "finished candidate gathering")
    });
    this.audel = document.createElement("audio");
    this.connection.addEventListener("track", (ev) => {
      this.audel.srcObject = ev.streams[0];
      this.audel.autoplay = true;
      this.audel.controls = true;
      this.client.outputAudio.appendChild(this.audel);
      console.log('got track :)');
    });
  }

  public async offer() {
    console.log('offering!');
    const offer = await this.connection.createOffer({ offerToReceiveAudio: true });
    await this.connection.setLocalDescription(offer);
    this.client.writeEvent({
      kind: WriteKind.Offer,
      to: this.peerId,
      offerSdp: offer.sdp ?? throwExpr("it didn't create an offer???"),
    });
  }

  public async gotOffer(offer: string) {
    console.log('got offer!');
    await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "offer", sdp: offer }));
    const answer = await this.connection.createAnswer({ offerToReceiveAudio: true });
    await this.connection.setLocalDescription(answer);

    this.client.writeEvent({
      kind: WriteKind.Answer,
      to: this.peerId,
      answerSdp: answer.sdp ?? throwExpr("it didn't create an offer???"),
    });
  }

  public async gotAnswer(answer: string) {
    console.log('got answer!');
    await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answer }));
  }
  public async gotCandidate(candidate: RTCIceCandidateInit) {
    console.log('got candidate...');
    await this.connection.addIceCandidate(candidate)
  }
}

function throwExpr(...args: Parameters<ErrorConstructor>): never {
  throw new Error(...args);
}
