"use client";

import { ajustesDoFiltro, efeitosPossiveis, querDesfoque, temEfeito, type Efeitos } from "./preferencias";
import type { ProcessadorDeCamera } from "./processador";

/*
 * A câmera de quem manda vídeo, com ou sem efeitos.
 *
 * - Sem efeito nenhum, a `saida` é a própria trilha da câmera: nada é
 *   processado, custo zero.
 * - Com efeito, a `saida` é a trilha que sai do processador (WebGL). Mexer na
 *   força dos efeitos só muda os números do processador: a trilha é a mesma e
 *   nada é renegociado.
 * - Ligar ou desligar os efeitos troca a `saida`: quem usa recebe
 *   `aoTrocarSaida` e troca a trilha no envio com replaceTrack (sem negociar
 *   de novo). Só depois disso o processador velho é desligado.
 *
 * O código pesado (WebGL e o MediaPipe) só é baixado na primeira vez que um
 * efeito é ligado com a câmera aberta.
 */

export type EstadoDosEfeitos = {
  /* O fundo: "carregando" enquanto baixa o modelo que acha a pessoa. */
  desfoque: "desligado" | "carregando" | "pronto" | "falhou";
  /* O aparelho processa vídeo? Sem isso os efeitos ficam indisponíveis. */
  possivel: boolean;
  aviso: string | null;
};

export class CameraComEfeitos {
  readonly bruta: MediaStreamTrack;
  /* Chamado quando a trilha que sai muda; o processador velho só desliga depois que a promessa termina. */
  aoTrocarSaida: (nova: MediaStreamTrack, antiga: MediaStreamTrack) => Promise<void> | void = () => undefined;

  private processador: ProcessadorDeCamera | null = null;
  private atual: MediaStreamTrack;
  private efeitos: Efeitos | null = null;
  private fila: Promise<unknown> = Promise.resolve();
  private ligada = true;
  private encerrada = false;
  private estadoAtual: EstadoDosEfeitos;
  private readonly ouvintes = new Set<() => void>();

  constructor(bruta: MediaStreamTrack) {
    this.bruta = bruta;
    this.atual = bruta;
    this.estadoAtual = { desfoque: "desligado", possivel: efeitosPossiveis(), aviso: null };
  }

  /* A trilha que vai para a sala (e para a prévia). */
  get saida(): MediaStreamTrack {
    return this.atual;
  }

  get estado(): EstadoDosEfeitos {
    return this.estadoAtual;
  }

  /* Para o React (useSyncExternalStore). */
  assinar = (ouvinte: () => void) => {
    this.ouvintes.add(ouvinte);
    return () => {
      this.ouvintes.delete(ouvinte);
    };
  };

  private mudarEstado(parcial: Partial<EstadoDosEfeitos>) {
    this.estadoAtual = { ...this.estadoAtual, ...parcial };
    for (const o of this.ouvintes) o();
  }

  /* Liga e desliga a imagem (a câmera continua aberta), como o botão da prévia. */
  definirLigada(ligada: boolean) {
    this.ligada = ligada;
    this.bruta.enabled = ligada;
    if (this.atual !== this.bruta) this.atual.enabled = ligada;
  }

  /* Aplica os efeitos escolhidos. Em fila: cliques rápidos não se atropelam. */
  aplicar(efeitos: Efeitos): Promise<void> {
    this.efeitos = efeitos;
    const tarefa = this.fila.then(() => this.aplicarAgora());
    this.fila = tarefa.catch(() => undefined);
    return tarefa;
  }

  private async aplicarAgora() {
    const e = this.efeitos;
    if (!e || this.encerrada) return;
    const querProcessar = temEfeito(e) && this.estadoAtual.possivel;

    if (querProcessar && !this.processador) {
      try {
        const { ProcessadorDeCamera } = await import("./processador");
        if (this.encerrada) return;
        const p = new ProcessadorDeCamera(this.bruta);
        p.aoFalhar = (motivo) => void this.quandoFalhar(p, motivo);
        p.saida.enabled = this.ligada;
        this.processador = p;
      } catch (erro) {
        console.warn("[live] efeitos indisponíveis", erro);
        this.mudarEstado({ possivel: false, aviso: "Este aparelho não consegue aplicar efeitos. A câmera segue normal." });
      }
    }

    const p = this.processador;
    if (querProcessar && p) {
      const desfoque = querDesfoque(e);
      p.ajustar({ ...ajustesDoFiltro(e), desfoque: desfoque ? e.desfoque / 100 : 0 });
      if (desfoque) this.prepararDesfoque(p);
      else if (this.estadoAtual.desfoque !== "falhou") this.mudarEstado({ desfoque: "desligado" });
      await this.trocarPara(p.saida);
    } else {
      await this.trocarPara(this.bruta);
      if (this.estadoAtual.desfoque === "carregando" || this.estadoAtual.desfoque === "pronto") this.mudarEstado({ desfoque: "desligado" });
      if (p) {
        this.processador = null;
        p.encerrar();
      }
    }
  }

  private async trocarPara(nova: MediaStreamTrack) {
    if (nova === this.atual) return;
    const antiga = this.atual;
    this.atual = nova;
    try {
      await this.aoTrocarSaida(nova, antiga);
    } catch (erro) {
      console.warn("[live] trocar a trilha da câmera", erro);
    }
  }

  /* Baixa o modelo que acha a pessoa (uma vez por aba) e entrega ao processador. */
  private prepararDesfoque(p: ProcessadorDeCamera) {
    /* Já falhou nesta câmera: não tenta de novo a cada mexida no controle. */
    if (this.estadoAtual.desfoque !== "desligado") return;
    this.mudarEstado({ desfoque: "carregando", aviso: null });
    void import("./segmentador")
      .then(({ carregarSegmentador }) => carregarSegmentador())
      .then((seg) => {
        if (this.processador !== p || this.encerrada) return;
        p.usarSegmentador(seg);
        this.mudarEstado({ desfoque: this.efeitos && querDesfoque(this.efeitos) ? "pronto" : "desligado" });
      })
      .catch((erro) => {
        console.warn("[live] desfoque indisponível", erro);
        if (this.processador !== p) return;
        this.mudarEstado({ desfoque: "falhou", aviso: "Não deu para desfocar o fundo neste aparelho. A câmera segue normal." });
      });
  }

  private async quandoFalhar(p: ProcessadorDeCamera, motivo: string) {
    console.warn("[live] efeitos", motivo);
    this.mudarEstado({ possivel: false, desfoque: "desligado", aviso: "Os efeitos pararam neste aparelho. A câmera segue normal, sem efeito." });
    const tarefa = this.fila.then(async () => {
      if (this.processador !== p) return;
      await this.trocarPara(this.bruta);
      this.processador = null;
      p.encerrar();
    });
    this.fila = tarefa.catch(() => undefined);
    await tarefa;
  }

  /* Medidas do processamento (para testes e diagnóstico). */
  estatisticas() {
    return this.processador?.estatisticas() ?? null;
  }

  encerrar() {
    if (this.encerrada) return;
    this.encerrada = true;
    this.processador?.encerrar();
    this.processador = null;
    this.bruta.stop();
    this.ouvintes.clear();
  }
}
