import { caminhoDasCurvas, comAlcas } from "./curvas";
import { geometria, papelRasgado, type Peca, type PecaDeForma, type PecaDeTraco, type PontoDoTraco } from "./documento";
import { nosDaForma } from "./forma-em-curvas";

/*
 * Soldar, Aparar e Interseção (as "formas" do Corel): duas ou mais formas
 * escolhidas viram UM desenho só, com o contorno de verdade (nada de bolinha
 * da cor do fundo por cima). Valem a forma pronta (qualquer uma) e o desenho
 * fechado da caneta; texto, foto e ícone ficam de fora.
 *
 * - Soldar: junta tudo num contorno.
 * - Aparar: as de cima recortam a de baixo (e saem; Ctrl+Z volta).
 * - Interseção: fica só o pedaço em que todas se encostam.
 * - Frente menos trás: a de cima, sem o que as de baixo cobrem.
 *
 * O resultado é um desenho fechado (traço) com a pintura, a borda, a sombra e
 * os efeitos da peça que recebeu o corte; os furos e os pedaços soltos vão em
 * `contornos`. "Separar" desfaz o desenho em peças, um pedaço (com os furos
 * dele) em cada uma. A conta é do paper.js (curvas Bézier exatas, sem virar
 * polígono), carregado só na hora do clique.
 */

export const OPERACOES_DE_FORMA = ["soldar", "aparar", "intersecao", "frente-menos-tras"] as const;
export type OperacaoDeForma = (typeof OPERACOES_DE_FORMA)[number];

type Paper = paper.PaperScope;
type Caminho = paper.PathItem;

/** A peça entra na conta: forma pronta ou desenho fechado. */
export function entraNoRecorte(p: Peca): p is PecaDeForma | PecaDeTraco {
  if (p.oculta) return false;
  if (p.tipo === "forma") return true;
  return p.tipo === "traco" && p.fechado && p.pontos.length >= 3;
}

const numero = (v: number) => Math.round(v * 1000) / 1000;

/*
 * Os caminhos SVG da peça na caixa dela (0 a w, 0 a h), sem giro nem espelho.
 * Cada parte vem com a escala que falta (as formas do documento são
 * desenhadas numa caixa de 100 × 100; o rasgado e o resto já em px).
 */
export function caminhosNaCaixa(p: PecaDeForma | PecaDeTraco): { partes: Array<{ d: string; sx: number; sy: number }>; parImpar: boolean } {
  const w = Math.max(1, p.w);
  const h = Math.max(1, p.h);
  if (p.tipo === "traco") {
    const principal = caminhoDasCurvas(comAlcas(p.pontos, true, p.suave), true, false, w, h, numero);
    const outros = (p.contornos || []).map((c) => caminhoDasCurvas(c, true, false, w, h, numero));
    return { partes: [{ d: [principal, ...outros].join(" "), sx: 1, sy: 1 }], parImpar: outros.length > 0 };
  }
  const nos = p.forma === "retangulo" || p.forma === "circulo" ? nosDaForma(p) : null;
  if (nos) return { partes: [{ d: caminhoDasCurvas(nos, true, false, w, h, numero), sx: 1, sy: 1 }], parImpar: false };
  const rasgado = p.forma === "rasgado";
  const desenho = rasgado ? papelRasgado(w, h, p.pontas, p.profundidade) : geometria(p.forma, p.pontas, p.profundidade);
  const sx = rasgado ? 1 : w / 100;
  const sy = rasgado ? 1 : h / 100;
  const partes: Array<{ d: string; sx: number; sy: number }> = [];
  for (const m of desenho.matchAll(/points="([^"]+)"/g)) {
    const pares = m[1]
      .trim()
      .split(/\s+/)
      .map((par) => par.split(",").map(Number))
      .filter((par) => par.length === 2 && par.every(Number.isFinite));
    if (pares.length >= 3) partes.push({ d: `M${pares.map(([x, y]) => `${x} ${y}`).join(" L")} Z`, sx, sy });
  }
  for (const m of desenho.matchAll(/\sd="([^"]+)"/g)) partes.push({ d: m[1], sx, sy });
  if (!partes.length) partes.push({ d: `M0 0 L${numero(w)} 0 L${numero(w)} ${numero(h)} L0 ${numero(h)} Z`, sx: 1, sy: 1 });
  return { partes, parImpar: /evenodd/.test(desenho) };
}

/** A peça como caminho do paper, já na arte (espelho e giro no centro da caixa, como no desenho). */
function caminhoDaPeca(ps: Paper, p: PecaDeForma | PecaDeTraco): Caminho {
  const { partes, parImpar } = caminhosNaCaixa(p);
  const filhos = partes.map(({ d, sx, sy }) => {
    const c = ps.PathItem.create(d);
    if (sx !== 1 || sy !== 1) c.scale(sx, sy, new ps.Point(0, 0));
    return c;
  });
  const caminho = filhos.slice(1).reduce<Caminho>((a, b) => a.unite(b), filhos[0]);
  if (parImpar) caminho.fillRule = "evenodd";
  const centro = new ps.Point(p.w / 2, p.h / 2);
  if (p.espelharX || p.espelharY) caminho.scale(p.espelharX ? -1 : 1, p.espelharY ? -1 : 1, centro);
  if (p.rot) caminho.rotate(p.rot, centro);
  caminho.translate(new ps.Point(p.x, p.y));
  return caminho;
}

/** Os caminhos simples (um contorno cada) de um resultado do paper. */
function filhosDe(c: Caminho): paper.Path[] {
  return c.className === "CompoundPath" ? ((c as paper.CompoundPath).children as paper.Path[]) : [c as paper.Path];
}

type Pedaco = { pontos: PontoDoTraco[]; area: number };

/** Os contornos do resultado em pontos da arte (alças do paper são relativas ao nó). */
function pedacosDoCaminho(c: Caminho): Pedaco[] {
  return filhosDe(c)
    .filter((f) => f.segments?.length >= 2)
    .map((f) => {
      const pontos = f.segments.map((s) => {
        const no: PontoDoTraco = { x: s.point.x, y: s.point.y };
        if (!s.handleIn.isZero()) no.e = { x: s.point.x + s.handleIn.x, y: s.point.y + s.handleIn.y };
        if (!s.handleOut.isZero()) no.s = { x: s.point.x + s.handleOut.x, y: s.point.y + s.handleOut.y };
        return no;
      });
      return { pontos, area: f.area };
    })
    .filter((p) => p.pontos.length >= 2 && Math.abs(p.area) > 0.5);
}

/** O desenho fechado com os contornos dados (na arte), herdando o jeito da peça modelo. */
function tracoDosContornos(modelo: PecaDeForma | PecaDeTraco, contornos: PontoDoTraco[][], nome: string, id = modelo.id): PecaDeTraco {
  const espessura = modelo.tipo === "traco" ? modelo.espessura : modelo.borda && modelo.borda.largura > 0 ? modelo.borda.largura : 0;
  const folga = Math.max(2, espessura);
  let x0 = Infinity;
  let y0 = Infinity;
  let x1 = -Infinity;
  let y1 = -Infinity;
  for (const c of contornos)
    for (const q of c)
      for (const v of [q, q.e, q.s]) {
        if (!v) continue;
        x0 = Math.min(x0, v.x);
        y0 = Math.min(y0, v.y);
        x1 = Math.max(x1, v.x);
        y1 = Math.max(y1, v.y);
      }
  const x = x0 - folga;
  const y = y0 - folga;
  const w = Math.max(1, x1 - x0 + folga * 2);
  const h = Math.max(1, y1 - y0 + folga * 2);
  const fracao = (v: { x: number; y: number }) => ({ x: Math.round(((v.x - x) / w) * 1e5) / 1e5, y: Math.round(((v.y - y) / h) * 1e5) / 1e5 });
  const emFracao = contornos.map((c) => c.map((q) => ({ ...fracao(q), ...(q.e ? { e: fracao(q.e) } : {}), ...(q.s ? { s: fracao(q.s) } : {}) })));
  const [pontos, ...resto] = emFracao;
  return {
    id,
    tipo: "traco",
    nome,
    x,
    y,
    w,
    h,
    rot: 0,
    opacidade: modelo.opacidade,
    ...(modelo.transparencia ? { transparencia: modelo.transparencia } : {}),
    ...(modelo.mistura ? { mistura: modelo.mistura } : {}),
    ...(modelo.efeitos?.length ? { efeitos: modelo.efeitos } : {}),
    ...(modelo.grupo ? { grupo: modelo.grupo } : {}),
    ...(modelo.fixa ? { fixa: true } : {}),
    ...(modelo.dentroDe ? { dentroDe: modelo.dentroDe } : {}),
    pontos,
    ...(resto.length ? { contornos: resto } : {}),
    fechado: true,
    suave: false,
    cor: modelo.tipo === "traco" ? modelo.cor : modelo.borda?.cor || "#141414",
    espessura,
    tracejado: modelo.tipo === "traco" ? modelo.tracejado : "solido",
    inicio: "nada",
    fim: "nada",
    preenchimento: modelo.tipo === "traco" ? modelo.preenchimento : modelo.pintura,
    sombra: modelo.sombra,
  };
}

let carregado: Promise<Paper> | null = null;
/** O paper.js numa tela de mentira (só a conta; nada é desenhado por ele). */
export function carregarPaper(): Promise<Paper> {
  carregado ??= import("paper/dist/paper-core").then((m) => {
    const raiz = ((m as unknown as { default?: Paper }).default ?? m) as unknown as Paper;
    const ps = new raiz.PaperScope();
    ps.setup(new ps.Size(10, 10));
    return ps;
  });
  return carregado;
}

export type ResultadoDaOperacao = { ok: true; peca: PecaDeTraco; saem: string[] } | { ok: false; erro: string };

const NOMES: Record<OperacaoDeForma, string> = { soldar: "Formas soldadas", aparar: "Forma aparada", intersecao: "Interseção", "frente-menos-tras": "Frente menos trás" };

/**
 * A operação nas peças escolhidas, na ordem da arte (a primeira é a de baixo).
 * Devolve a peça nova (no lugar da que recebeu o corte, com o id dela) e as
 * que saem da arte.
 */
export async function operarFormas(pecas: Peca[], operacao: OperacaoDeForma): Promise<ResultadoDaOperacao> {
  const formas = pecas.filter(entraNoRecorte);
  if (formas.length < 2 || formas.length !== pecas.length) return { ok: false, erro: "Escolha só formas e desenhos fechados (duas ou mais). Texto, foto e ícone não entram." };
  const ps = await carregarPaper();
  ps.project.clear();
  const caminhos = formas.map((p) => caminhoDaPeca(ps, p));
  const debaixo = formas[0];
  const decima = formas[formas.length - 1];
  let resultado: Caminho;
  let modelo: PecaDeForma | PecaDeTraco = debaixo;
  if (operacao === "soldar") resultado = caminhos.slice(1).reduce((a, b) => a.unite(b), caminhos[0]);
  else if (operacao === "intersecao") resultado = caminhos.slice(1).reduce((a, b) => a.intersect(b), caminhos[0]);
  else if (operacao === "aparar") resultado = caminhos.slice(1).reduce((a, b) => a.subtract(b), caminhos[0]);
  else {
    modelo = decima;
    resultado = caminhos.slice(0, -1).reduce((a, b) => a.subtract(b), caminhos[caminhos.length - 1]);
  }
  const alvo = caminhos[formas.indexOf(modelo)];
  const encostam = caminhos.some((c) => c !== alvo && (c.intersects(alvo) || alvo.contains(c.interiorPoint) || c.contains(alvo.interiorPoint)));
  const pedacos = pedacosDoCaminho(resultado);
  ps.project.clear();
  if (!pedacos.length) return { ok: false, erro: operacao === "intersecao" ? "As formas não se encostam: não sobra nada em comum." : "Não sobrou nada da forma." };
  if (!encostam && operacao !== "soldar") return { ok: false, erro: "As formas não se encostam. Ponha uma em cima da outra e tente de novo." };
  /* O maior pedaço vai em `pontos` (a ferramenta Forma mexe nele); o resto vira contornos. */
  const ordem = [...pedacos].sort((a, b) => Math.abs(b.area) - Math.abs(a.area));
  if (ordem.reduce((t, p) => t + p.pontos.length, 0) > 2000) return { ok: false, erro: "O desenho ficou com pontos demais. Use formas mais simples." };
  const peca = tracoDosContornos(modelo, ordem.map((p) => p.pontos), NOMES[operacao]);
  return { ok: true, peca, saem: formas.filter((p) => p.id !== modelo.id).map((p) => p.id) };
}

/** O desenho tem mais de um pedaço solto (dá para separar). */
export function daParaSeparar(p: Peca): p is PecaDeTraco {
  return p.tipo === "traco" && p.fechado && Boolean(p.contornos?.length);
}

/**
 * "Separar": cada pedaço solto do desenho vira uma peça (os furos ficam no
 * pedaço que os tem por fora). A primeira peça fica com o id do desenho.
 */
export async function separarContornos(p: PecaDeTraco, novoId: () => string): Promise<PecaDeTraco[]> {
  if (!daParaSeparar(p)) return [p];
  const ps = await carregarPaper();
  ps.project.clear();
  const caminho = caminhoDaPeca(ps, p);
  const filhos = filhosDe(caminho);
  /* Por fora: não fica dentro de nenhum outro; furo: o menor de fora que o contém. */
  const dentroDe = (a: paper.Path, b: paper.Path) => a !== b && Math.abs(b.area) > Math.abs(a.area) && b.contains(a.interiorPoint) && !a.intersects(b);
  const profundidade = (f: paper.Path) => filhos.filter((o) => dentroDe(f, o)).length;
  const deFora = filhos.filter((f) => profundidade(f) % 2 === 0).sort((a, b) => Math.abs(b.area) - Math.abs(a.area));
  const pedacos = deFora.map((fora) => {
    const furos = filhos.filter((f) => profundidade(f) % 2 === 1 && dentroDe(f, fora) && profundidade(f) === profundidade(fora) + 1);
    return [fora, ...furos].flatMap((f) => pedacosDoCaminho(f).map((q) => q.pontos));
  });
  ps.project.clear();
  const nome = p.nome?.replace(/ \(\d+\)$/, "") || "Pedaço";
  return pedacos
    .filter((contornos) => contornos.length)
    .map((contornos, i) => {
      const t = tracoDosContornos({ ...p, rot: 0, espelharX: false, espelharY: false }, contornos, `${nome} (${i + 1})`, i === 0 ? p.id : novoId());
      return { ...t, grupo: undefined };
    });
}
