import type { PontoDoTraco } from "./documento";

/*
 * As curvas do traço (caneta Bézier, desenho livre e a ferramenta Forma, como
 * no Corel). Cada nó tem a posição e, se for curva, as alças: "e" (entrada,
 * puxa a curva que chega) e "s" (saída, puxa a que sai), na mesma medida dos
 * pontos (fração da caixa). Nó "suave" mantém as duas alças em linha (mexeu
 * numa, a outra gira junto); "canto" deixa cada uma livre. Sem alças, o
 * trecho é reto. O desenho "suave" antigo (curva automática pelos pontos) é
 * o mesmo que alças calculadas (Catmull-Rom); `comAlcas` escreve elas.
 */

type Par = { x: number; y: number };
export type Segmento = { i: number; j: number; a: Par; c1: Par; c2: Par; b: Par; reto: boolean };

const soma = (p: Par, q: Par): Par => ({ x: p.x + q.x, y: p.y + q.y });
const menos = (p: Par, q: Par): Par => ({ x: p.x - q.x, y: p.y - q.y });
const vezes = (p: Par, k: number): Par => ({ x: p.x * k, y: p.y * k });
const entre = (p: Par, q: Par, t: number): Par => ({ x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t });
const tamanho = (p: Par) => Math.hypot(p.x, p.y);

/** Algum nó tem alça (o traço é desenhado pelas alças, não pela curva automática). */
export function temAlcas(pontos: PontoDoTraco[]) {
  return pontos.some((q) => q.e || q.s);
}

/**
 * As alças escritas: o desenho "suave" antigo vira alças de verdade (a mesma
 * curva), e o resto fica como está. É o que a ferramenta Forma mexe.
 */
export function comAlcas(pontos: PontoDoTraco[], fechado: boolean, suave: boolean): PontoDoTraco[] {
  if (!suave || temAlcas(pontos) || pontos.length < 3) return pontos.map((q) => ({ ...q }));
  const n = pontos.length;
  const ponto = (i: number) => (fechado ? pontos[(i + n) % n] : pontos[Math.max(0, Math.min(n - 1, i))]);
  return pontos.map((q, i) => {
    const d = vezes(menos(ponto(i + 1), ponto(i - 1)), 1 / 6);
    const primeiro = !fechado && i === 0;
    const ultimo = !fechado && i === n - 1;
    return { x: q.x, y: q.y, e: primeiro ? null : menos(q, d), s: ultimo ? null : soma(q, d), no: "suave" as const };
  });
}

/** Os trechos do traço (cada um vai de um nó ao próximo; fechado tem o trecho de volta ao primeiro). */
export function segmentos(pontos: PontoDoTraco[], fechado: boolean, suave: boolean): Segmento[] {
  const nos = comAlcas(pontos, fechado, suave);
  const lista: Segmento[] = [];
  const total = fechado ? nos.length : nos.length - 1;
  for (let i = 0; i < total; i += 1) {
    const j = (i + 1) % nos.length;
    const a = nos[i];
    const b = nos[j];
    lista.push({ i, j, a, b, c1: a.s ?? a, c2: b.e ?? b, reto: !a.s && !b.e });
  }
  return lista;
}

/** O caminho SVG nas medidas dadas (fração × largura e altura). */
export function caminhoDasCurvas(pontos: PontoDoTraco[], fechado: boolean, suave: boolean, w: number, h: number, numero: (v: number) => string | number = (v) => Math.round(v * 100) / 100) {
  if (pontos.length < 2) return "";
  const em = (p: Par) => `${numero(p.x * w)} ${numero(p.y * h)}`;
  let d = `M${em(pontos[0])}`;
  for (const s of segmentos(pontos, fechado, suave)) d += s.reto ? ` L${em(s.b)}` : ` C${em(s.c1)} ${em(s.c2)} ${em(s.b)}`;
  return fechado ? `${d} Z` : d;
}

/** Um ponto da curva do trecho em t (0 a 1). */
export function noTrecho(s: Segmento, t: number): Par {
  const u = 1 - t;
  return {
    x: u * u * u * s.a.x + 3 * u * u * t * s.c1.x + 3 * u * t * t * s.c2.x + t * t * t * s.b.x,
    y: u * u * u * s.a.y + 3 * u * u * t * s.c1.y + 3 * u * t * t * s.c2.y + t * t * t * s.b.y,
  };
}

/** O lugar da curva mais perto de (x, y), em px (w × h): qual trecho, em que t e a que distância. */
export function maisPerto(pontos: PontoDoTraco[], fechado: boolean, suave: boolean, w: number, h: number, x: number, y: number) {
  let melhor = { trecho: -1, t: 0, distancia: Infinity };
  segmentos(pontos, fechado, suave).forEach((s, k) => {
    const distancia = (t: number) => {
      const p = noTrecho(s, t);
      return Math.hypot(p.x * w - x, p.y * h - y);
    };
    let t0 = 0;
    let d0 = Infinity;
    for (let passo = 0; passo <= 32; passo += 1) {
      const d = distancia(passo / 32);
      if (d < d0) {
        d0 = d;
        t0 = passo / 32;
      }
    }
    /* Afina em volta do melhor passo. */
    let largura = 1 / 32;
    for (let volta = 0; volta < 12; volta += 1) {
      largura /= 2;
      for (const t of [t0 - largura, t0 + largura]) {
        if (t < 0 || t > 1) continue;
        const d = distancia(t);
        if (d < d0) {
          d0 = d;
          t0 = t;
        }
      }
    }
    if (d0 < melhor.distancia) melhor = { trecho: k, t: t0, distancia: d0 };
  });
  return melhor;
}

/** Põe um nó no trecho em t sem mudar o desenho (divide a curva em duas). */
export function dividirTrecho(pontos: PontoDoTraco[], fechado: boolean, suave: boolean, trecho: number, t: number): { pontos: PontoDoTraco[]; novo: number } {
  const nos = comAlcas(pontos, fechado, suave);
  const s = segmentos(nos, fechado, false)[trecho];
  if (!s) return { pontos: nos, novo: -1 };
  const novo = s.i + 1;
  if (s.reto) {
    const meio = entre(s.a, s.b, t);
    nos.splice(novo, 0, { x: meio.x, y: meio.y });
    return { pontos: nos, novo };
  }
  /* De Casteljau: as alças novas dos dois lados e do nó do meio. */
  const p01 = entre(s.a, s.c1, t);
  const p12 = entre(s.c1, s.c2, t);
  const p23 = entre(s.c2, s.b, t);
  const p012 = entre(p01, p12, t);
  const p123 = entre(p12, p23, t);
  const meio = entre(p012, p123, t);
  nos[s.i] = { ...nos[s.i], s: p01 };
  nos[s.j] = { ...nos[s.j], e: p23 };
  nos.splice(novo, 0, { x: meio.x, y: meio.y, e: p012, s: p123, no: "suave" });
  return { pontos: nos, novo };
}

/** A alça do outro lado acompanha (nó suave): mesma direção ao contrário, mantendo o tamanho dela. */
function alinharOposta(no: PontoDoTraco, mexida: "e" | "s"): PontoDoTraco {
  const outra = mexida === "e" ? "s" : "e";
  const alca = no[mexida];
  const oposta = no[outra];
  if (!alca || !oposta || no.no === "canto") return no;
  const v = menos(no, alca);
  const comprimento = tamanho(v);
  if (comprimento < 1e-6) return no;
  const k = tamanho(menos(oposta, no)) / comprimento;
  return { ...no, [outra]: soma(no, vezes(v, k)) };
}

/**
 * Entortar o trecho arrastando a linha (como a ferramenta Forma do Corel): o
 * ponto pego em t anda o quanto o mouse andou, e as alças do trecho se
 * ajustam para a curva passar por ele. Trecho reto vira curva.
 */
export function entortarTrecho(original: PontoDoTraco[], fechado: boolean, suave: boolean, trecho: number, t: number, delta: Par): PontoDoTraco[] {
  const nos = comAlcas(original, fechado, suave);
  const s = segmentos(nos, fechado, false)[trecho];
  if (!s) return nos;
  const a = nos[s.i];
  const b = nos[s.j];
  const c1 = a.s ?? entre(a, b, 1 / 3);
  const c2 = b.e ?? entre(a, b, 2 / 3);
  const tt = Math.max(0.08, Math.min(0.92, t));
  const u = 1 - tt;
  const k = 1 / (3 * tt * u * (u * u + tt * tt));
  nos[s.i] = alinharOposta({ ...a, s: soma(c1, vezes(delta, k * u)) }, "s");
  nos[s.j] = alinharOposta({ ...b, e: soma(c2, vezes(delta, k * tt)) }, "e");
  return nos;
}

/** Mexe numa alça; nó suave gira a do outro lado junto (Alt solta: o nó vira canto). */
export function moverAlca(pontos: PontoDoTraco[], indice: number, qual: "e" | "s", para: Par, soltar: boolean): PontoDoTraco[] {
  return pontos.map((q, i) => {
    if (i !== indice) return q;
    const mexido: PontoDoTraco = { ...q, [qual]: para, ...(soltar ? { no: "canto" as const } : {}) };
    return soltar ? mexido : alinharOposta(mexido, qual);
  });
}

/** Mexe nos nós escolhidos (as alças andam junto). */
export function moverNos(pontos: PontoDoTraco[], indices: number[], delta: Par): PontoDoTraco[] {
  return pontos.map((q, i) =>
    indices.includes(i) ? { ...q, x: q.x + delta.x, y: q.y + delta.y, e: q.e ? soma(q.e, delta) : q.e, s: q.s ? soma(q.s, delta) : q.s } : q,
  );
}

/** Tira os nós (sobram pelo menos 2; fechado, pelo menos 3). */
export function apagarNos(pontos: PontoDoTraco[], indices: number[], fechado: boolean): PontoDoTraco[] | null {
  const resto = pontos.filter((_, i) => !indices.includes(i));
  return resto.length >= (fechado ? 3 : 2) ? resto : null;
}

export type TipoDoNo = "curva" | "reta" | "canto" | "suave";

/**
 * Muda os nós escolhidos: "curva" põe alças (na direção dos vizinhos), "reta"
 * tira as alças (os trechos dali ficam retos), "canto" solta as alças uma da
 * outra, "suave" alinha as duas.
 */
export function mudarNos(pontos: PontoDoTraco[], indices: number[], tipo: TipoDoNo, fechado: boolean): PontoDoTraco[] {
  const n = pontos.length;
  return pontos.map((q, i) => {
    if (!indices.includes(i)) return q;
    if (tipo === "reta") return { x: q.x, y: q.y };
    if (tipo === "canto") return { ...q, no: "canto" };
    const antes = fechado ? pontos[(i - 1 + n) % n] : pontos[Math.max(0, i - 1)];
    const depois = fechado ? pontos[(i + 1) % n] : pontos[Math.min(n - 1, i + 1)];
    if (tipo === "curva" && !(q.e || q.s)) {
      const d = vezes(menos(depois, antes), 1 / 6);
      const primeiro = !fechado && i === 0;
      const ultimo = !fechado && i === n - 1;
      return { x: q.x, y: q.y, e: primeiro ? null : menos(q, d), s: ultimo ? null : soma(q, d), no: "suave" };
    }
    /* Suave: as duas alças na mesma linha (a média das direções), cada uma com o seu tamanho. */
    const e = q.e ?? menos(q, vezes(menos(depois, antes), 1 / 6));
    const s = q.s ?? soma(q, vezes(menos(depois, antes), 1 / 6));
    const ve = menos(q, e);
    const vs = menos(s, q);
    const direcao = soma(vezes(ve, 1 / (tamanho(ve) || 1)), vezes(vs, 1 / (tamanho(vs) || 1)));
    const unidade = vezes(direcao, 1 / (tamanho(direcao) || 1));
    return { ...q, e: q.e === null && !fechado && i === 0 ? null : menos(q, vezes(unidade, tamanho(ve))), s: q.s === null && !fechado && i === n - 1 ? null : soma(q, vezes(unidade, tamanho(vs))), no: "suave" };
  });
}

/** A caixa da curva (não das alças), em quem os pontos estiverem medidos. */
export function limitesDaCurva(pontos: PontoDoTraco[], fechado: boolean, suave: boolean) {
  let x0 = Infinity;
  let y0 = Infinity;
  let x1 = -Infinity;
  let y1 = -Infinity;
  const ver = (p: Par) => {
    x0 = Math.min(x0, p.x);
    y0 = Math.min(y0, p.y);
    x1 = Math.max(x1, p.x);
    y1 = Math.max(y1, p.y);
  };
  pontos.forEach(ver);
  for (const s of segmentos(pontos, fechado, suave)) {
    if (s.reto) continue;
    for (let passo = 1; passo < 24; passo += 1) ver(noTrecho(s, passo / 24));
  }
  return { x0, y0, x1, y1 };
}
