import { v4 } from "uuid";
import { derived, get, type Readable, type Writable, writable } from "svelte/store";
import { readHello } from "./protocol/hello";
import { BinaryStreamReader } from "./binary/readerStream";
import { ReadKind, readEvent } from "./protocol/serverEvent";
import { AudioManager, PlayerAudio } from "./audio";
import { type WriteEvent, writeEvent, WriteKind } from "./protocol/clientEvent";
import { BinaryWriter } from "./binary/writer";
import { cached, type CachedWritable, type Cached, cachedWritable } from "./stores";
import { BinaryReader } from "./binary/reader";

export class Client {
  static async connect(name: Readable<string>, 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 array = new Uint8Array(0x20);
    new TextEncoder().encodeInto(get(name), array);
    await writer.write(array);

    const reader = BinaryStreamReader.fromReader(stream.readable.getReader());
    const { players, peers } = await readHello(reader);
    console.log("done reading hello");

    const client = new Client(audioManager, outputAudio, name, cachedWritable(players), uuid, transport, stream, writer);
    globalThis.client = client;

    for (const [id, name] of peers) {
      const peer = client.getPeer(id);
      peer.targetPlayerName.set(name);
      peer.offer();
    }

    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 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 datagramEventHandler() {
    const dgramReader = this.transport.datagrams.readable.getReader();
    this.eventHandler(async () => {
      const dgram = await dgramReader.read();
      return readEvent(new BinaryReader(new DataView(dgram.value.buffer)));
    })
  }

  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.update(x => Object.assign(x, { [event.id]: new Player(event.name, event.id) }))
          break;
        }
        case ReadKind.Disconnected: {
          console.log("goodbye", event.id);
          delete this.players[event.id];
          this.players.update(x => {
            delete x[event.id];
            return x;
          })
          break;
          break;
        }
        case ReadKind.Moved: {
          const player = this.players.get()[event.id];
          if (player)
            player.stageInfo.update(info => Object.assign(info, { position: event.position }));
          break;
        }
        case ReadKind.StageChanged: {
          console.log("goodbye", event.id);
          const player = this.players.get()[event.id];
          if (player)
            player.stageInfo.update(info => Object.assign(info, { stage: event.stage }));
          break;
        }
        case ReadKind.PeerConnectionChanged: {
          console.log("peer changed", event.id, event.connected, event.target);
          if (event.connected)
            this.getPeer(event.id).targetPlayerName.set(event.target);
          else
            delete this.peers[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> = {};
  public boundPlayer: Readable<Player | undefined>;
  public playersByName: Cached<Record<string, Player>>;

  private constructor(
    public audioManager: AudioManager,
    public outputAudio: HTMLDivElement,
    boundPlayerName: Readable<string>,
    public players: CachedWritable<Record<number, Player>>,
    public uuid: string,
    public transport: WebTransport,
    public stream: WebTransportBidirectionalStream,
    public writer: WritableStreamDefaultWriter,
  ) {
    this.playersByName = cached(this.players, (players) => {
      console.log("players by name derivation", Object.values(players), Object.fromEntries(Object.values(players).map(x => [x.name, x])))
      return Object.fromEntries(Object.values(players).map(x => [x.name, x]))
    })
    this.boundPlayer = derived([boundPlayerName, this.playersByName], ([playerName, players]) => players[playerName]);
  }

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

export class Player {
  public stageInfo: Writable<{ position: number[], stage: string }>;
  constructor(public name: string, public id: number, stageInfo?: { position: number[], stage: string }) {

    this.stageInfo = writable(Object.assign({ position: [0, 0, 0], stage: "nostage" + Math.random() }, stageInfo ?? {}));
  }
}

export class Peer {
  private connection: RTCPeerConnection;
  private playerAudio: PlayerAudio;
  public targetPlayerName: Writable<string>;
  private targetPlayer: Readable<Player | undefined>;
  private state: Writable<string> = writable("not connected");
  constructor(public peerId: string, public client: Client) {
    this.connection = new RTCPeerConnection({
      iceServers: [
        {
          urls: ["stun:stun.l.google.com:19302"],
        }
      ]
    });
    this.targetPlayerName = writable("");
    this.targetPlayer = derived([this.targetPlayerName, this.client.playersByName], ([playerName, players]) => players[playerName]);
    this.playerAudio = this.client.audioManager.createPlayerAudio(this.client.outputAudio, this.client.boundPlayer, this.targetPlayer);
    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.set(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.connection.addEventListener("track", (ev) => {
      this.playerAudio.gotTrack(ev.streams[0]);
    });
  }

  public async offer() {
    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) {
    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) {
    await this.connection.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answer }));
  }
  public async gotCandidate(candidate: RTCIceCandidateInit) {
    await this.connection.addIceCandidate(candidate)
  }
}

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