import { escalarPeca, podeSerRecipiente, type DocumentoDoBanner, type Peca } from "./documento";

/*
 * "Colocar dentro" como o PowerClip do Corel: o conteúdo (a peça com
 * `dentroDe`) aparece só dentro da silhueta do recipiente e passa a ser
 * parte dele.
 *  - entra como no Corel: em cima do recipiente, fica onde está; todo fora,
 *    vai para o meio (ou leva o alinhamento que tinha com ele);
 *  - anda, cresce e gira junto com o recipiente, fica logo acima dele nas
 *    camadas e sai junto quando ele é apagado (comConteudosJuntos, que o
 *    editor aplica a cada mudança da arte);
 *  - as coordenadas continuam as da arte: o servidor, as miniaturas e o
 *    assistente desenham como antes.
 * Só um nível: o recipiente que está dentro de outro não conta como recipiente.
 */

type Caixa = { x: number; y: number; w: number; h: number; rot: number };

const mesmaCaixa = (a: Caixa, b: Caixa) => a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h && a.rot === b.rot;

function girar(x: number, y: number, graus: number) {
  if (!graus) return { x, y };
  const r = (graus * Math.PI) / 180;
  return { x: x * Math.cos(r) - y * Math.sin(r), y: x * Math.sin(r) + y * Math.cos(r) };
}

/** O recipiente da peça na lista (null = ela não está dentro de nada que exista). */
export function recipienteDe(p: Peca, pecas: Peca[]): Peca | null {
  if (!p.dentroDe) return null;
  const r = pecas.find((q) => q.id === p.dentroDe);
  return r && r.id !== p.id && !(r.dentroDe && pecas.some((q) => q.id === r.dentroDe)) ? r : null;
}

/** O que está dentro da peça, na ordem das camadas. */
export function conteudosDe(id: string, pecas: Peca[]): Peca[] {
  if (!pecas.some((q) => q.dentroDe === id)) return [];
  return pecas.filter((q) => q.dentroDe === id && recipienteDe(q, pecas)?.id === id);
}

/** O recipiente aberto para ajustar o conteúdo: as escolhidas são todas conteúdo dele. */
export function recipienteAberto(ids: string[], pecas: Peca[]): Peca | null {
  if (!ids.length) return null;
  const primeira = pecas.find((p) => p.id === ids[0]);
  const r = primeira ? recipienteDe(primeira, pecas) : null;
  return r && ids.every((id) => pecas.find((p) => p.id === id)?.dentroDe === r.id) ? r : null;
}

/* A caixa das peças nos eixos do recipiente (o meio dele é o zero; girado, desfeito o giro). */
function caixaNoRecipiente(pecas: Peca[], r: Peca) {
  const cx = r.x + r.w / 2;
  const cy = r.y + r.h / 2;
  const pontos = pecas.flatMap((p) =>
    [
      [-1, -1],
      [1, -1],
      [1, 1],
      [-1, 1],
    ].map(([sx, sy]) => {
      const v = girar((sx * p.w) / 2, (sy * p.h) / 2, p.rot);
      return girar(p.x + p.w / 2 + v.x - cx, p.y + p.h / 2 + v.y - cy, -r.rot);
    }),
  );
  return { x1: Math.min(...pontos.map((q) => q.x)), x2: Math.max(...pontos.map((q) => q.x)), y1: Math.min(...pontos.map((q) => q.y)), y2: Math.max(...pontos.map((q) => q.y)) };
}

type Alinhamento = "inicio" | "meio" | "fim";

/**
 * O conteúdo entrando no recipiente, como o PowerClip do Corel ("centralizar
 * só quando está todo fora"):
 *  - em cima do recipiente (encostando nele): entra onde está, do jeito que
 *    está;
 *  - todo fora: vai para dentro, no meio. Se estava alinhado com o
 *    recipiente (topo, base, esquerda, direita ou o meio de um lado), leva o
 *    alinhamento: alinhada no topo, entra encostada no topo. O tamanho e o
 *    giro ficam os dela (ajustar é com os dois cliques, como no Corel).
 */
export function encaixarNoRecipiente(conteudos: Peca[], r: Peca): Peca[] {
  if (!conteudos.length) return conteudos;
  const W = r.w / 2;
  const H = r.h / 2;
  const antes = caixaNoRecipiente(conteudos, r);
  const encosta = antes.x1 < W - 0.5 && antes.x2 > -W + 0.5 && antes.y1 < H - 0.5 && antes.y2 > -H + 0.5;
  if (encosta) return conteudos;
  /* Alinhado com o recipiente (um ou dois pixels de folga, ou 1% do tamanho dele). */
  const folga = Math.max(2, Math.max(r.w, r.h) * 0.01);
  const alinhamento = (a: number, b: number, meio: number): Alinhamento => (Math.abs(a + meio) <= folga ? "inicio" : Math.abs(b - meio) <= folga ? "fim" : "meio");
  const ax = alinhamento(antes.x1, antes.x2, W);
  const ay = alinhamento(antes.y1, antes.y2, H);
  const [lw, lh] = [antes.x2 - antes.x1, antes.y2 - antes.y1];
  const alvo = (a: Alinhamento, meio: number, lado: number) => (a === "inicio" ? -meio + lado / 2 : a === "fim" ? meio - lado / 2 : 0);
  const d = girar(alvo(ax, W, lw) - (antes.x1 + antes.x2) / 2, alvo(ay, H, lh) - (antes.y1 + antes.y2) / 2, r.rot);
  return conteudos.map((c) => ({ ...c, x: c.x + d.x, y: c.y + d.y }));
}

/**
 * O conteúdo acompanhando o recipiente que mudou de `antes` para `depois`: o
 * meio dele vai para o mesmo ponto relativo, o giro soma e o tamanho segue a
 * escala por igual (sem deformar). Esticado de um lado só, o conteúdo cresce
 * só o que precisa para continuar cobrindo o recipiente (se cobria), como no
 * Canva; puxar e voltar não deixa ele crescendo à toa.
 */
export function acompanharRecipiente<T extends Peca>(c: T, antes: Caixa, depois: Caixa): T {
  if (antes.w === depois.w && antes.h === depois.h && antes.rot === depois.rot) return { ...c, x: c.x + depois.x - antes.x, y: c.y + depois.y - antes.y };
  const sx = depois.w / Math.max(1e-6, antes.w);
  const sy = depois.h / Math.max(1e-6, antes.h);
  /* O meio do conteúdo nos eixos do recipiente de antes, levado para o de depois. */
  const local = girar(c.x + c.w / 2 - (antes.x + antes.w / 2), c.y + c.h / 2 - (antes.y + antes.h / 2), -antes.rot);
  const lx = local.x * sx;
  const ly = local.y * sy;
  const novo = girar(lx, ly, depois.rot);
  let f = Math.min(sx, sy);
  if (Math.abs(sx - sy) > 1e-9 * Math.max(sx, sy)) {
    const relativo = (((c.rot - antes.rot) % 180) + 180) % 180;
    const alinhado = relativo < 0.01 || relativo > 179.99;
    const cobria = alinhado && Math.abs(local.x) + antes.w / 2 <= c.w / 2 + 0.5 && Math.abs(local.y) + antes.h / 2 <= c.h / 2 + 0.5;
    if (cobria) f = Math.max(f, (Math.abs(lx) + depois.w / 2) / (c.w / 2), (Math.abs(ly) + depois.h / 2) / (c.h / 2));
  }
  const e = Math.abs(f - 1) < 1e-9 ? c : escalarPeca(c, f);
  return { ...e, x: depois.x + depois.w / 2 + novo.x - e.w / 2, y: depois.y + depois.h / 2 + novo.y - e.h / 2, rot: c.rot + depois.rot - antes.rot };
}

/**
 * A arte depois de uma mudança, com cada conteúdo junto do recipiente:
 *  - recipiente apagado leva o conteúdo junto (o conteúdo que não mudou);
 *  - recipiente que andou, cresceu ou girou leva o conteúdo (o que não se
 *    mexeu na mesma conta: com os dois escolhidos, os dois já andaram);
 *  - o conteúdo fica logo acima do recipiente nas camadas (frente e trás
 *    levam os dois juntos).
 * Sem nada dentro de nada, devolve a mesma arte.
 */
export function comConteudosJuntos(antes: DocumentoDoBanner, depois: DocumentoDoBanner): DocumentoDoBanner {
  if (antes === depois || !depois.pecas.some((p) => p.dentroDe)) return depois;
  const doAntes = new Map(antes.pecas.map((p) => [p.id, p]));
  const ids = new Set(depois.pecas.map((p) => p.id));
  let mudou = false;
  let pecas = depois.pecas.filter((p) => {
    const sai = Boolean(p.dentroDe && !ids.has(p.dentroDe) && doAntes.has(p.dentroDe) && doAntes.get(p.id) === p);
    if (sai) mudou = true;
    return !sai;
  });
  const agora = new Map(pecas.map((p) => [p.id, p]));
  const recipiente = (p: Peca) => {
    const r = p.dentroDe ? agora.get(p.dentroDe) : undefined;
    return r && r.id !== p.id && !(r.dentroDe && agora.has(r.dentroDe)) ? r : undefined;
  };
  pecas = pecas.map((p) => {
    const r = recipiente(p);
    const r0 = r && doAntes.get(r.id);
    const p0 = doAntes.get(p.id);
    if (!r || !r0 || !p0 || mesmaCaixa(r0, r) || !mesmaCaixa(p0, p)) return p;
    mudou = true;
    return acompanharRecipiente(p, r0, r);
  });
  const filhos = new Map<string, Peca[]>();
  for (const p of pecas) {
    const r = recipiente(p);
    if (r) filhos.set(r.id, [...(filhos.get(r.id) || []), p]);
  }
  if (filhos.size) {
    const ordem: Peca[] = [];
    for (const p of pecas) {
      if (recipiente(p)) continue;
      ordem.push(p);
      const dentro = filhos.get(p.id);
      if (dentro) ordem.push(...dentro);
    }
    if (ordem.length === pecas.length && ordem.some((p, i) => p !== pecas[i])) {
      pecas = ordem;
      mudou = true;
    }
  }
  return mudou ? { ...depois, pecas } : depois;
}

/** Por que o recipiente não recebe essas peças (null = recebe). */
export function porQueNaoRecebe(r: Peca, conteudos: Peca[], pecas: Peca[]): string | null {
  if (!conteudos.length) return "Escolha antes o que vai dentro.";
  if (conteudos.some((c) => c.id === r.id)) return "Clique em outra peça: essa é a que vai dentro.";
  if (conteudos.some((c) => conteudosDe(c.id, pecas).length)) return "Tire antes o que está dentro dela (uma coisa dentro da outra vai até um nível).";
  if (r.tipo === "texto") {
    if (conteudos.length !== 1 || conteudos[0].tipo !== "imagem") return "Texto recebe só uma foto (as letras viram a janela da foto).";
    return podeSerRecipiente(r, conteudos[0]) ? null : "Texto em curva não recebe foto.";
  }
  return conteudos.every((c) => podeSerRecipiente(r, c)) ? null : "Essa peça não recebe nada dentro: use uma forma, um desenho fechado, uma foto recortada ou um texto.";
}

/**
 * As peças entrando no recipiente: encaixadas nele, logo acima dele (e do
 * que já estava dentro). O grupo fica só se foi inteiro para dentro.
 */
export function colocarDentroDe(pecas: Peca[], ids: string[], recipienteId: string): Peca[] {
  const r = pecas.find((p) => p.id === recipienteId);
  if (!r) return pecas;
  const conteudos = pecas.filter((p) => ids.includes(p.id) && p.id !== r.id);
  if (!conteudos.length) return pecas;
  const levados = new Set(conteudos.map((c) => c.id));
  const grupoInteiro = (g?: string) => Boolean(g) && pecas.filter((p) => p.grupo === g).every((p) => levados.has(p.id));
  const encaixados = encaixarNoRecipiente(conteudos, r).map((c) => ({ ...c, dentroDe: r.id, grupo: grupoInteiro(c.grupo) ? c.grupo : undefined }) as Peca);
  const resto = pecas.filter((p) => !levados.has(p.id));
  const onde = resto.reduce((ultimo, p, i) => (p.id === r.id || p.dentroDe === r.id ? i : ultimo), -1);
  return [...resto.slice(0, onde + 1), ...encaixados, ...resto.slice(onde + 1)];
}

/** As peças com o que está dentro delas, logo depois de cada uma (copiar e duplicar levam o recipiente com o conteúdo). */
export function comConteudos(escolhidas: Peca[], pecas: Peca[]): Peca[] {
  const vistas = new Set(escolhidas.map((p) => p.id));
  return escolhidas.flatMap((p) => {
    const dentro = conteudosDe(p.id, pecas).filter((c) => !vistas.has(c.id));
    for (const c of dentro) vistas.add(c.id);
    return [p, ...dentro];
  });
}
