/*
 * A composição livre relida em outro tamanho (o passo 2 do "Redimensionar",
 * ver redimensionar.ts): fundo estica, faixas continuam de ponta a ponta,
 * decorações acompanham o meio e o conteúdo vira blocos que se empilham ou se
 * deitam conforme a proporção nova. Separado para os modelos de post
 * (modelos-colecao.ts) se adaptarem a qualquer tamanho sem import circular.
 */
import { escalarPeca, type Peca, type PecaDeTexto } from "./documento";

type Caixa = { x: number; y: number; w: number; h: number };
type Orientacao = "faixa" | "quadrado" | "alto";

const orientacao = (w: number, h: number): Orientacao => (w / h >= 1.6 ? "faixa" : h / w >= 1.25 ? "alto" : "quadrado");
export const cobre = (p: Peca, W: number, H: number) => p.x <= 1 && p.y <= 1 && p.x + p.w >= W - 1 && p.y + p.h >= H - 1;
const area = (c: Caixa) => Math.max(0, c.w) * Math.max(0, c.h);

function caixaDe(pecas: Array<Pick<Peca, "x" | "y" | "w" | "h">>): Caixa {
  const x = Math.min(...pecas.map((p) => p.x));
  const y = Math.min(...pecas.map((p) => p.y));
  return { x, y, w: Math.max(...pecas.map((p) => p.x + p.w)) - x, h: Math.max(...pecas.map((p) => p.y + p.h)) - y };
}

export function intersecao(a: Caixa, b: Caixa) {
  const w = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
  const h = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
  return w > 0 && h > 0 ? w * h : 0;
}

/* A peça na escala f, já no lugar (x, y) novo. */
function noLugar<T extends Peca>(p: T, f: number, x: number, y: number): T {
  return { ...escalarPeca(p, f), x, y };
}

/* ====================================================== faixas e decoração */

/* O texto repetido de uma faixa ("BLACK FRIDAY • ATÉ 70% OFF • ...") completa a largura nova. */
export function completarRepeticao(p: PecaDeTexto): PecaDeTexto {
  if (p.html || !p.texto.includes("•")) return p;
  const partes = p.texto.split("•").map((x) => x.trim()).filter(Boolean);
  if (partes.length < 2) return p;
  let periodo = partes.length;
  for (let k = 1; k <= partes.length / 2; k += 1) {
    if (partes.every((parte, i) => i < k || parte === partes[i - k] || i >= partes.length - 1)) {
      periodo = k;
      break;
    }
  }
  const unidade = `${partes.slice(0, periodo).join("  •  ")}  •  `;
  const vezes = Math.max(2, Math.floor(p.w / (unidade.length * 0.43 * p.tamanho)));
  return { ...p, texto: unidade.repeat(vezes).trim() };
}

/* Uma faixa de ponta a ponta continua de ponta a ponta, presa à borda de onde estava. */
function faixaNova(p: Peca, W: number, H: number, W2: number, H2: number): Peca {
  const sMenor = Math.min(W2, H2) / Math.min(W, H);
  const deitada = p.w >= p.h;
  const escalada = escalarPeca(p, sMenor);
  if (deitada) {
    const w = (p.w / W) * W2;
    const centro = (p.y + p.h / 2) / H;
    const y = centro < 0.34 ? p.y * sMenor : centro > 0.66 ? H2 - (H - p.y) * sMenor : centro * H2 - escalada.h / 2;
    const pronta = { ...escalada, x: (p.x / W) * W2, w, y } as Peca;
    return pronta.tipo === "texto" ? completarRepeticao(pronta) : pronta;
  }
  const h = (p.h / H) * H2;
  const centro = (p.x + p.w / 2) / W;
  const x = centro < 0.34 ? p.x * sMenor : centro > 0.66 ? W2 - (W - p.x) * sMenor : centro * W2 - escalada.w / 2;
  return { ...escalada, y: (p.y / H) * H2, h, x } as Peca;
}

/* Decoração grande (círculo de fundo, "%" gigante): acompanha o meio, no tamanho da área. */
function decoracaoNova(p: Peca, W: number, H: number, W2: number, H2: number): Peca {
  const f = Math.sqrt((W2 / W) * (H2 / H));
  const cx = ((p.x + p.w / 2) / W) * W2;
  const cy = ((p.y + p.h / 2) / H) * H2;
  return noLugar(p, f, cx - (p.w * f) / 2, cy - (p.h * f) / 2);
}

/* Peça solta no mesmo lugar relativo, na escala do lado menor, sem sair da arte. */
export function noMesmoLugar(p: Peca, W: number, H: number, W2: number, H2: number): Peca {
  if (cobre(p, W, H)) return { ...p, x: 0, y: 0, w: W2, h: H2 } as Peca;
  if (ehFaixa(p, W, H)) return faixaNova(p, W, H, W2, H2);
  const f = Math.min(W2, H2) / Math.min(W, H);
  const w = p.w * f;
  const h = p.h * f;
  const x = Math.min(Math.max(((p.x + p.w / 2) / W) * W2 - w / 2, -w * 0.3), W2 - w * 0.7);
  const y = Math.min(Math.max(((p.y + p.h / 2) / H) * H2 - h / 2, -h * 0.3), H2 - h * 0.7);
  return noLugar(p, f, x, y);
}

/*
 * Peça própria que caiu em cima de um texto ou da foto do modelo: procura o
 * lugar livre mais perto (varre a arte numa grade e fica com o de menos
 * sobreposição, desempatando pela distância do lugar de antes).
 */
export function longeDoConteudo(p: Peca, ocupado: Peca[], W2: number, H2: number): Peca {
  if (cobre(p, W2, H2) || ehFaixa(p, W2, H2)) return p;
  const sobreposicao = (x: number, y: number) => ocupado.reduce((s, o) => s + intersecao({ x, y, w: p.w, h: p.h }, o), 0);
  if (sobreposicao(p.x, p.y) < p.w * p.h * 0.08) return p;
  let melhor = { x: p.x, y: p.y, nota: Infinity };
  const passos = 16;
  for (let i = 0; i <= passos; i += 1) {
    for (let j = 0; j <= passos; j += 1) {
      const x = (i / passos) * (W2 - p.w);
      const y = (j / passos) * (H2 - p.h);
      const nota = sobreposicao(x, y) / (p.w * p.h) + Math.hypot(x - p.x, y - p.y) / Math.max(W2, H2);
      if (nota < melhor.nota) melhor = { x, y, nota };
    }
  }
  return { ...p, x: melhor.x, y: melhor.y } as Peca;
}

/* Faixa = de ponta a ponta E comprida (um círculo grande não é faixa). */
export function ehFaixa(p: Peca, W: number, H: number) {
  return (p.w >= W * 0.8 && p.h <= H * 0.4 && p.w / Math.max(1, p.h) >= 3) || (p.h >= H * 0.8 && p.w <= W * 0.4 && p.h / Math.max(1, p.w) >= 3);
}

/* A forma está atrás de uma foto ou de um texto (brilho, anel, fundo do botão): anda com ela. */
function atrasDeConteudo(p: Peca, todas: Peca[]) {
  if (p.tipo !== "forma" && p.tipo !== "textura") return false;
  return todas.some(
    (o) =>
      o !== p &&
      (o.tipo === "imagem" || o.tipo === "texto" || o.tipo === "icone") &&
      o.x + o.w / 2 > p.x &&
      o.x + o.w / 2 < p.x + p.w &&
      o.y + o.h / 2 > p.y &&
      o.y + o.h / 2 < p.y + p.h &&
      p.w * p.h >= o.w * o.h * 0.6 &&
      p.w * p.h <= o.w * o.h * 6,
  );
}

/* ============================================================ os blocos */

type Bloco = { pecas: Peca[]; caixa: Caixa; temFoto: boolean };

/* Junta o que anda junto: a forma atrás da foto (brilho, anel), o texto em cima de um botão. */
function blocos(pecas: Peca[]): Bloco[] {
  const pai = pecas.map((_, i) => i);
  const raiz = (i: number): number => (pai[i] === i ? i : (pai[i] = raiz(pai[i])));
  const unir = (a: number, b: number) => {
    pai[raiz(a)] = raiz(b);
  };
  const fundoDeOutra = (a: Peca) => a.tipo === "forma" || a.tipo === "textura";
  for (let i = 0; i < pecas.length; i += 1) {
    for (let j = i + 1; j < pecas.length; j += 1) {
      const a = pecas[i];
      const b = pecas[j];
      const comum = intersecao(a, b);
      if (!comum) continue;
      const menor = Math.min(area(a), area(b));
      const centroDentro = (de: Peca, em: Peca) => de.x + de.w / 2 > em.x && de.x + de.w / 2 < em.x + em.w && de.y + de.h / 2 > em.y && de.y + de.h / 2 < em.y + em.h;
      const atras = (fundoDeOutra(a) && centroDentro(b, a)) || (fundoDeOutra(b) && centroDentro(a, b));
      if (atras || comum > menor * 0.5) unir(i, j);
    }
  }
  const grupos = new Map<number, Peca[]>();
  pecas.forEach((p, i) => grupos.set(raiz(i), [...(grupos.get(raiz(i)) || []), p]));
  return [...grupos.values()].map((lista) => ({ pecas: lista, caixa: caixaDe(lista), temFoto: lista.some((p) => p.tipo === "imagem" && p.papel === "foto") }));
}

/*
 * Quanto o texto ocupa ao quebrar as linhas numa largura nova: pelo número de
 * letras de cada linha escrita (largura média de uma letra ≈ 0,52 da fonte).
 * É estimativa; o editor mede no palco depois e ajusta o que passar.
 */
function alturaQuebrada(p: PecaDeTexto, f: number, largura: number) {
  const letra = p.tamanho * f * (0.52 + Math.max(0, p.espaco));
  const linhas = p.texto.split("\n").reduce((soma, linha) => soma + Math.max(1, Math.ceil((linha.length * letra) / Math.max(1, largura * 0.96))), 0);
  return Math.max(p.h * f * 0.5, linhas * p.tamanho * f * p.linha * 1.06);
}

/* Muda o alinhamento dos textos do bloco (pilha centrada ou coluna à esquerda). */
function alinharTextos(pecas: Peca[], alinhamento: PecaDeTexto["alinhamento"]) {
  return pecas.map((p) => (p.tipo === "texto" && p.alinhamento !== alinhamento ? { ...p, alinhamento } : p));
}

/* Põe o bloco com o canto em (x, y), na escala f, mantendo o arranjo de dentro. */
function porBloco(b: Bloco, f: number, x: number, y: number): Peca[] {
  return b.pecas.map((p) => noLugar(p, f, x + (p.x - b.caixa.x) * f, y + (p.y - b.caixa.y) * f));
}

/* Colunas: blocos que se cruzam na horizontal ficam na mesma coluna. */
function colunas(lista: Bloco[]): Bloco[][] {
  const ordem = [...lista].sort((a, b) => a.caixa.x - b.caixa.x);
  const saida: Array<{ x: number; x2: number; blocos: Bloco[] }> = [];
  for (const b of ordem) {
    const atual = saida.at(-1);
    const sobra = atual ? Math.min(atual.x2, b.caixa.x + b.caixa.w) - Math.max(atual.x, b.caixa.x) : 0;
    if (atual && sobra > Math.min(atual.x2 - atual.x, b.caixa.w) * 0.3) {
      atual.blocos.push(b);
      atual.x = Math.min(atual.x, b.caixa.x);
      atual.x2 = Math.max(atual.x2, b.caixa.x + b.caixa.w);
    } else saida.push({ x: b.caixa.x, x2: b.caixa.x + b.caixa.w, blocos: [b] });
  }
  return saida.map((c) => c.blocos.sort((a, b) => a.caixa.y - b.caixa.y));
}

/*
 * Empilha blocos de cima para baixo (respiro original entre eles), cada um
 * centrado ou à esquerda da área. O tamanho sai da ALTURA disponível: um
 * texto que ficaria mais largo que a área quebra as linhas de novo (a caixa
 * fica da largura da área e cresce para baixo), como um designer faria ao
 * passar uma faixa para um story. Fotos e blocos com forma não quebram:
 * eles limitam a escala pela largura.
 */
function empilharBlocos(lista: Bloco[], alvo: Caixa, centrar: boolean, maximo: number): Peca[] {
  if (!lista.length) return [];
  const respiros = lista.map((b, i) => (i === 0 ? 0 : Math.max(0, Math.min(b.caixa.y - (lista[i - 1].caixa.y + lista[i - 1].caixa.h), lista[i - 1].caixa.h))));
  const minimo = Math.max(...lista.map((b) => b.caixa.h)) * 0.08;
  const vaos = respiros.map((r, i) => (i === 0 ? 0 : Math.max(r, minimo)));
  const soTexto = (b: Bloco) => b.pecas.length === 1 && b.pecas[0].tipo === "texto";
  const larguraFixa = Math.max(1, ...lista.filter((b) => !soTexto(b)).map((b) => b.caixa.w));
  /* Altura de cada bloco na escala f (texto largo demais quebra em mais linhas). */
  const alturaNa = (b: Bloco, f: number) => (soTexto(b) && b.caixa.w * f > alvo.w ? alturaQuebrada(b.pecas[0] as PecaDeTexto, f, alvo.w) : b.caixa.h * f);
  const total = (f: number) => lista.reduce((s, b, i) => s + alturaNa(b, f) + vaos[i] * f, 0);
  let f = Math.min(maximo, alvo.w / larguraFixa, alvo.h / Math.max(1, total(1)));
  for (let volta = 0; volta < 24 && total(f) > alvo.h; volta += 1) f *= 0.94;
  /* Sobrou altura: cresce até encher (sem passar do teto e da largura das fotos). */
  for (let volta = 0; volta < 24; volta += 1) {
    const maior = f * 1.04;
    if (maior > maximo || maior > alvo.w / larguraFixa || total(maior) > alvo.h) break;
    f = maior;
  }
  let y = alvo.y + (alvo.h - total(f)) / 2;
  const saida: Peca[] = [];
  lista.forEach((b, i) => {
    y += vaos[i] * f;
    const h = alturaNa(b, f);
    if (soTexto(b) && b.caixa.w * f > alvo.w) {
      const p = b.pecas[0];
      const escalada = escalarPeca(p, f);
      saida.push(...alinharTextos([{ ...escalada, x: alvo.x, y, w: alvo.w, h } as Peca], centrar ? "centro" : "esquerda"));
    } else {
      const x = centrar ? alvo.x + (alvo.w - b.caixa.w * f) / 2 : alvo.x;
      saida.push(...alinharTextos(porBloco(b, f, x, y), centrar ? "centro" : "esquerda"));
    }
    y += h;
  });
  return saida;
}

/** As peças soltas (sem modelo, ou postas pela pessoa) reorganizadas no tamanho novo. */
export function reorganizarPecas(pecas: Peca[], W: number, H: number, W2: number, H2: number): Peca[] {
  const fundos: Peca[] = [];
  const faixas: Peca[] = [];
  const decoracoes: Peca[] = [];
  const conteudo: Peca[] = [];
  for (const p of pecas) {
    if (cobre(p, W, H)) fundos.push({ ...p, x: (p.x / W) * W2, y: (p.y / H) * H2, w: p.w + (W2 - W), h: p.h + (H2 - H) } as Peca);
    else if (ehFaixa(p, W, H)) faixas.push(faixaNova(p, W, H, W2, H2));
    else if (atrasDeConteudo(p, pecas)) conteudo.push(p);
    else if ((p.tipo === "forma" || p.tipo === "textura" || p.opacidade < 0.35) && (p.w >= W * 0.6 || p.h >= H * 0.6)) decoracoes.push(decoracaoNova(p, W, H, W2, H2));
    else conteudo.push(p);
  }
  if (!conteudo.length) return [...fundos, ...decoracoes, ...faixas];

  /* O espaço livre: a arte menos uma margem e as faixas presas nas bordas. */
  const m = Math.min(W2, H2) * 0.07;
  let topo = m;
  let base = H2 - m;
  for (const f of faixas) {
    if (f.w < f.h) continue;
    if (f.y + f.h / 2 < H2 * 0.34) topo = Math.max(topo, f.y + f.h + m * 0.4);
    else if (f.y + f.h / 2 > H2 * 0.66) base = Math.min(base, f.y - m * 0.4);
  }
  const livre: Caixa = { x: m, y: topo, w: W2 - m * 2, h: Math.max(H2 * 0.3, base - topo) };
  const crescer = (Math.min(W2, H2) / Math.min(W, H)) * 1.6;

  const lista = blocos(conteudo);
  const de = orientacao(W, H);
  const para = orientacao(W2, H2);
  let arrumadas: Peca[];

  if (de === para) {
    /* Mesma cara: o conjunto ocupa o espaço novo, com o meio no mesmo lugar relativo. */
    const c = caixaDe(conteudo);
    const f = Math.min(livre.w / c.w, livre.h / c.h, crescer);
    const cx = Math.min(Math.max(((c.x + c.w / 2) / W) * W2, livre.x + (c.w * f) / 2), livre.x + livre.w - (c.w * f) / 2);
    const cy = Math.min(Math.max(((c.y + c.h / 2) / H) * H2, livre.y + (c.h * f) / 2), livre.y + livre.h - (c.h * f) / 2);
    arrumadas = porBloco({ pecas: conteudo, caixa: c, temFoto: false }, f, cx - (c.w * f) / 2, cy - (c.h * f) / 2);
  } else if (para === "faixa") {
    /* Deitando: texto à esquerda, foto à direita (sem foto, tudo à esquerda). */
    const comFoto = lista.filter((b) => b.temFoto);
    const semFoto = lista.filter((b) => !b.temFoto).sort((a, b) => a.caixa.y - b.caixa.y);
    if (!comFoto.length || !semFoto.length) {
      arrumadas = empilharBlocos([...lista].sort((a, b) => a.caixa.y - b.caixa.y), { ...livre, w: livre.w * 0.62 }, false, crescer);
    } else {
      const ladoDaFoto = Math.min(livre.h, livre.w * 0.4);
      const areaDaFoto: Caixa = { x: livre.x + livre.w - ladoDaFoto, y: livre.y, w: ladoDaFoto, h: livre.h };
      const areaDoTexto: Caixa = { x: livre.x, y: livre.y, w: areaDaFoto.x - livre.x - m, h: livre.h };
      const foto = { pecas: comFoto.flatMap((b) => b.pecas), caixa: caixaDe(comFoto.flatMap((b) => b.pecas)), temFoto: true };
      const fFoto = Math.min(areaDaFoto.w / foto.caixa.w, areaDaFoto.h / foto.caixa.h, crescer);
      arrumadas = [
        ...porBloco(foto, fFoto, areaDaFoto.x + (areaDaFoto.w - foto.caixa.w * fFoto) / 2, areaDaFoto.y + (areaDaFoto.h - foto.caixa.h * fFoto) / 2),
        ...empilharBlocos(semFoto, areaDoTexto, false, crescer),
      ];
    }
  } else {
    /* Ficando em pé (ou quadrada): as colunas viram uma pilha centrada. */
    const empilhados = colunas(lista).flat();
    arrumadas = empilharBlocos(empilhados, livre, true, crescer);
  }
  return [...fundos, ...decoracoes, ...faixas, ...arrumadas];
}
