/*
 * Os destaques que quem apresenta faz em cima da tela compartilhada: caneta,
 * marca-texto e o ponteiro laser (o rastro vermelho que some sozinho).
 *
 * As posições vão de 0 a 1 dentro da imagem da tela (não da janela): cada um
 * vê o risco no mesmo lugar, qualquer que seja o tamanho da tela dele. O
 * mesmo desenho serve para a sala e para a gravação.
 *
 * Arquivo sem navegador nem servidor: os testes rodam direto.
 */

export type Ferramenta = "caneta" | "marca" | "laser";

export const CORES_DA_CANETA = ["#ea4335", "#fbbc04", "#34a853", "#4285f4", "#ffffff"] as const;

/* O que viaja no canal (curto: vai muitas vezes por segundo). */
export type RecadoDaCaneta =
  /* Pontos novos de um risco (o risco nasce no primeiro recado com aquele id).
     `de`: a posição do primeiro número no risco; o mesmo pedaço chegando de novo não duplica. */
  | { k: "r"; id: string; f: "caneta" | "marca"; c: string; p: number[]; de?: number; fim?: boolean }
  /* O laser: onde o ponteiro está agora (vazio = saiu de cima da tela). */
  | { k: "l"; p: number[] }
  | { k: "desfazer"; id: string }
  | { k: "limpar" };

type Risco = { id: string; f: "caneta" | "marca"; c: string; pontos: number[]; fim: boolean };
type PontoDoLaser = { x: number; y: number; em: number };

/* Quanto tempo o rastro do laser fica na tela. */
export const VIDA_DO_LASER_MS = 650;
const MAX_RISCOS = 400;
const MAX_PONTOS_POR_RISCO = 4000;

function coordenada(n: unknown): number | null {
  return typeof n === "number" && Number.isFinite(n) && n >= -0.05 && n <= 1.05 ? Math.round(n * 10000) / 10000 : null;
}

/* Confere um recado que chegou de fora (já com a assinatura conferida). */
export function lerRecadoDaCaneta(bruto: unknown): RecadoDaCaneta | null {
  if (!bruto || typeof bruto !== "object" || Array.isArray(bruto)) return null;
  const o = bruto as Record<string, unknown>;
  const pontos = (valor: unknown, teto: number): number[] | null => {
    if (!Array.isArray(valor) || valor.length % 2 !== 0 || valor.length > teto) return null;
    const lista: number[] = [];
    for (const v of valor) {
      const n = coordenada(v);
      if (n === null) return null;
      lista.push(n);
    }
    return lista;
  };
  if (o.k === "limpar") return { k: "limpar" };
  if (o.k === "desfazer" && typeof o.id === "string" && /^[a-z0-9]{4,24}$/.test(o.id)) return { k: "desfazer", id: o.id };
  if (o.k === "l") {
    const p = pontos(o.p, 2);
    return p ? { k: "l", p } : null;
  }
  if (o.k === "r") {
    if (typeof o.id !== "string" || !/^[a-z0-9]{4,24}$/.test(o.id)) return null;
    if (o.f !== "caneta" && o.f !== "marca") return null;
    if (typeof o.c !== "string" || !(CORES_DA_CANETA as readonly string[]).includes(o.c)) return null;
    const p = pontos(o.p, 400);
    if (!p) return null;
    const de = typeof o.de === "number" && Number.isInteger(o.de) && o.de >= 0 && o.de < 1_000_000 ? o.de : undefined;
    return { k: "r", id: o.id, f: o.f, c: o.c, p, ...(de !== undefined ? { de } : {}), fim: o.fim === true };
  }
  return null;
}

export class QuadroDeAnotacoes {
  private riscos: Risco[] = [];
  private laser: PontoDoLaser[] = [];
  /* Muda a cada alteração: quem desenha sabe se precisa pintar de novo. */
  versao = 0;

  aplicar(recado: RecadoDaCaneta, agora = Date.now()) {
    this.versao += 1;
    if (recado.k === "limpar") {
      this.riscos = [];
      this.laser = [];
      return;
    }
    if (recado.k === "desfazer") {
      this.riscos = this.riscos.filter((r) => r.id !== recado.id);
      return;
    }
    if (recado.k === "l") {
      if (recado.p.length === 2) this.laser.push({ x: recado.p[0], y: recado.p[1], em: agora });
      else this.laser.push({ x: NaN, y: NaN, em: agora });
      this.limparLaser(agora);
      return;
    }
    let risco = this.riscos.find((r) => r.id === recado.id);
    if (!risco) {
      risco = { id: recado.id, f: recado.f, c: recado.c, pontos: [], fim: false };
      this.riscos.push(risco);
      if (this.riscos.length > MAX_RISCOS) this.riscos.shift();
    }
    if (recado.de !== undefined && risco.pontos.length >= recado.de + recado.p.length) {
      /* Este pedaço já chegou (a cópia para quem entrou depois). */
    } else if (recado.de !== undefined && recado.de < risco.pontos.length) {
      risco.pontos.push(...recado.p.slice(risco.pontos.length - recado.de));
    } else if (risco.pontos.length < MAX_PONTOS_POR_RISCO) risco.pontos.push(...recado.p);
    if (recado.fim) risco.fim = true;
  }

  private limparLaser(agora: number) {
    const corte = agora - VIDA_DO_LASER_MS;
    while (this.laser.length && this.laser[0].em < corte) this.laser.shift();
    if (this.laser.length > 120) this.laser.splice(0, this.laser.length - 120);
  }

  /* Tudo o que está riscado, em pedaços, para quem entrou depois. */
  exportar(): RecadoDaCaneta[] {
    const recados: RecadoDaCaneta[] = [];
    for (const r of this.riscos) {
      for (let de = 0; de < r.pontos.length; de += 400) {
        const p = r.pontos.slice(de, de + 400);
        recados.push({ k: "r", id: r.id, f: r.f, c: r.c, p, de, fim: r.fim && de + 400 >= r.pontos.length });
      }
    }
    return recados;
  }

  /* O último risco (para "desfazer"). */
  ultimoRisco(): string | null {
    return this.riscos[this.riscos.length - 1]?.id ?? null;
  }

  get vazio(): boolean {
    return !this.riscos.length && !this.laser.length;
  }

  /* Ainda tem laser sumindo: quem desenha continua pintando. */
  temLaser(agora = Date.now()): boolean {
    this.limparLaser(agora);
    return this.laser.length > 0;
  }

  /* Desenha tudo dentro do retângulo onde a tela compartilhada aparece. */
  desenhar(ctx: CanvasRenderingContext2D, area: { x: number; y: number; l: number; a: number }, agora = Date.now()) {
    const { x, y, l, a } = area;
    if (l <= 0 || a <= 0) return;
    const escala = Math.max(0.6, Math.min(2.4, l / 1000));
    ctx.save();
    ctx.beginPath();
    ctx.rect(x, y, l, a);
    ctx.clip();
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    for (const r of this.riscos) {
      if (r.pontos.length < 2) continue;
      ctx.globalAlpha = r.f === "marca" ? 0.38 : 1;
      ctx.strokeStyle = r.c;
      ctx.lineWidth = (r.f === "marca" ? 22 : 5) * escala;
      ctx.beginPath();
      ctx.moveTo(x + r.pontos[0] * l, y + r.pontos[1] * a);
      if (r.pontos.length === 2) ctx.lineTo(x + r.pontos[0] * l + 0.1, y + r.pontos[1] * a);
      for (let i = 2; i < r.pontos.length; i += 2) ctx.lineTo(x + r.pontos[i] * l, y + r.pontos[i + 1] * a);
      ctx.stroke();
    }
    ctx.globalAlpha = 1;
    this.limparLaser(agora);
    /* O rastro do laser: vai afinando e sumindo; a ponta brilha. */
    const pontos = this.laser;
    for (let i = 1; i < pontos.length; i += 1) {
      const p0 = pontos[i - 1];
      const p1 = pontos[i];
      if (!Number.isFinite(p0.x) || !Number.isFinite(p1.x)) continue;
      const vida = 1 - (agora - p1.em) / VIDA_DO_LASER_MS;
      if (vida <= 0) continue;
      ctx.strokeStyle = `rgba(234, 67, 53, ${0.85 * vida})`;
      ctx.lineWidth = 7 * escala * vida;
      ctx.beginPath();
      ctx.moveTo(x + p0.x * l, y + p0.y * a);
      ctx.lineTo(x + p1.x * l, y + p1.y * a);
      ctx.stroke();
    }
    const ponta = pontos[pontos.length - 1];
    if (ponta && Number.isFinite(ponta.x) && agora - ponta.em < VIDA_DO_LASER_MS * 3) {
      const cx = x + ponta.x * l;
      const cy = y + ponta.y * a;
      const brilho = ctx.createRadialGradient(cx, cy, 0, cx, cy, 16 * escala);
      brilho.addColorStop(0, "rgba(255, 90, 70, 0.95)");
      brilho.addColorStop(0.35, "rgba(234, 67, 53, 0.55)");
      brilho.addColorStop(1, "rgba(234, 67, 53, 0)");
      ctx.fillStyle = brilho;
      ctx.beginPath();
      ctx.arc(cx, cy, 16 * escala, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = "#fff";
      ctx.beginPath();
      ctx.arc(cx, cy, 2.6 * escala, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }
}

/* Onde a imagem aparece dentro de uma caixa com "object-fit: contain". */
export function areaDaImagem(caixaL: number, caixaA: number, imagemL: number, imagemA: number, x = 0, y = 0) {
  if (!imagemL || !imagemA || !caixaL || !caixaA) return { x, y, l: caixaL, a: caixaA };
  const escala = Math.min(caixaL / imagemL, caixaA / imagemA);
  const l = imagemL * escala;
  const a = imagemA * escala;
  return { x: x + (caixaL - l) / 2, y: y + (caixaA - a) / 2, l, a };
}

export function novoIdDeRisco(): string {
  return `${Date.now().toString(36)}${Math.floor(Math.random() * 1679616).toString(36)}`.slice(0, 20);
}
