/*
 * A perspectiva de um grupo (como o "Adicionar perspectiva" do Corel): os
 * 4 cantos da caixa do grupo são puxados e tudo dentro entorta junto, para
 * encaixar a arte num lugar (a frente de uma caixa, uma parede, a camiseta
 * deitada). Cada peça continua peça (texto continua texto): ela ganha a
 * própria distorção (os 4 cantos da caixa dela), tirada da mesma
 * homografia do grupo. A peça guarda a perspectiva do grupo (caixa e
 * cantos, em pixels da arte) para o grupo reabrir com os cantos onde
 * ficaram, mesmo depois de mover ou crescer o grupo inteiro.
 */

export type Ponto = [number, number];
export type Quadrilatero = [Ponto, Ponto, Ponto, Ponto];
export type CaixaReta = { x: number; y: number; w: number; h: number };
export type PerspectivaDoGrupo = { caixa: CaixaReta; cantos: Quadrilatero };

type PecaNaPerspectiva = { id: string; x: number; y: number; w: number; h: number; rot: number; perspectiva?: PerspectivaDoGrupo | null };

/** A homografia (3 × 3, em linha) que leva o quadrado unitário nos 4 cantos (sentido horário, do canto de cima à esquerda). */
function doQuadradoPara([[x0, y0], [x1, y1], [x2, y2], [x3, y3]]: Quadrilatero): number[] {
  const dx1 = x1 - x2;
  const dx2 = x3 - x2;
  const dx3 = x0 - x1 + x2 - x3;
  const dy1 = y1 - y2;
  const dy2 = y3 - y2;
  const dy3 = y0 - y1 + y2 - y3;
  let g = 0;
  let h = 0;
  if (Math.abs(dx3) > 1e-12 || Math.abs(dy3) > 1e-12) {
    const det = dx1 * dy2 - dx2 * dy1 || 1e-12;
    g = (dx3 * dy2 - dx2 * dy3) / det;
    h = (dx1 * dy3 - dx3 * dy1) / det;
  }
  return [x1 - x0 + g * x1, x3 - x0 + h * x3, x0, y1 - y0 + g * y1, y3 - y0 + h * y3, y0, g, h, 1];
}

/** Leva o ponto pela homografia. */
export function pelaHomografia(m: number[], x: number, y: number): Ponto {
  const w = m[6] * x + m[7] * y + m[8] || 1e-12;
  return [(m[0] * x + m[1] * y + m[2]) / w, (m[3] * x + m[4] * y + m[5]) / w];
}

/** A homografia da caixa reta para os 4 cantos (pontos da arte → pontos da arte). */
export function homografiaDaCaixa(caixa: CaixaReta, cantos: Quadrilatero): number[] {
  const q = doQuadradoPara(cantos);
  /* Primeiro a caixa vira o quadrado unitário (escala e translação), depois o quadrado vira os cantos. */
  const sx = 1 / (caixa.w || 1);
  const sy = 1 / (caixa.h || 1);
  const n = [sx, 0, -caixa.x * sx, 0, sy, -caixa.y * sy, 0, 0, 1];
  const r: number[] = [];
  for (let i = 0; i < 3; i += 1) for (let j = 0; j < 3; j += 1) r.push(q[i * 3] * n[j] + q[i * 3 + 1] * n[3 + j] + q[i * 3 + 2] * n[6 + j]);
  return r;
}

export const cantosDaCaixa = (c: CaixaReta): Quadrilatero => [
  [c.x, c.y],
  [c.x + c.w, c.y],
  [c.x + c.w, c.y + c.h],
  [c.x, c.y + c.h],
];

/* Os 4 cantos da peça na arte (com o giro em volta do meio). */
function cantosDaPeca(p: PecaNaPerspectiva): Quadrilatero {
  const r = (p.rot * Math.PI) / 180;
  const [cos, sen] = [Math.cos(r), Math.sin(r)];
  const cx = p.x + p.w / 2;
  const cy = p.y + p.h / 2;
  const leva = (lx: number, ly: number): Ponto => [cx + (lx - p.w / 2) * cos - (ly - p.h / 2) * sen, cy + (lx - p.w / 2) * sen + (ly - p.h / 2) * cos];
  return [leva(0, 0), leva(p.w, 0), leva(p.w, p.h), leva(0, p.h)];
}

/** A caixa reta que cobre as peças (giradas), sem a distorção. */
export function caixaDasPecas(pecas: PecaNaPerspectiva[]): CaixaReta {
  const pontos = pecas.flatMap(cantosDaPeca);
  const xs = pontos.map((p) => p[0]);
  const ys = pontos.map((p) => p[1]);
  const x = Math.min(...xs);
  const y = Math.min(...ys);
  return { x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y };
}

/**
 * Os cantos de agora do grupo: os guardados nas peças (todas com a mesma
 * perspectiva), levados da caixa de quando foi feita para a caixa de agora
 * (o grupo pode ter andado ou crescido); sem perspectiva, os da caixa.
 */
export function cantosDoGrupo(pecas: PecaNaPerspectiva[]): { caixa: CaixaReta; cantos: Quadrilatero; tem: boolean } {
  const caixa = caixaDasPecas(pecas);
  const guardada = pecas[0]?.perspectiva;
  const igual = guardada && pecas.every((p) => p.perspectiva && JSON.stringify(p.perspectiva) === JSON.stringify(guardada));
  if (!guardada || !igual) return { caixa, cantos: cantosDaCaixa(caixa), tem: false };
  const a = guardada.caixa;
  const leva = ([x, y]: Ponto): Ponto => [caixa.x + ((x - a.x) / (a.w || 1)) * caixa.w, caixa.y + ((y - a.y) / (a.h || 1)) * caixa.h];
  return { caixa, cantos: guardada.cantos.map(leva) as Quadrilatero, tem: true };
}

/**
 * A distorção de cada peça para o grupo inteiro ir para os `cantos`: os
 * cantos da peça passam pela homografia do grupo e voltam para a caixa
 * dela (o giro desfeito), em fração da largura e da altura dela.
 */
export function distorcoesDoGrupo(pecas: PecaNaPerspectiva[], caixa: CaixaReta, cantos: Quadrilatero) {
  const m = homografiaDaCaixa(caixa, cantos);
  const perspectiva: PerspectivaDoGrupo = { caixa: { ...caixa }, cantos: cantos.map(([x, y]) => [x, y]) as Quadrilatero };
  return pecas.map((p) => {
    const r = (-p.rot * Math.PI) / 180;
    const [cos, sen] = [Math.cos(r), Math.sin(r)];
    const cx = p.x + p.w / 2;
    const cy = p.y + p.h / 2;
    const local = cantosDaPeca(p).map(([x, y]) => {
      const [ax, ay] = pelaHomografia(m, x, y);
      const lx = (ax - cx) * cos - (ay - cy) * sen + p.w / 2;
      const ly = (ax - cx) * sen + (ay - cy) * cos + p.h / 2;
      return [lx / (p.w || 1), ly / (p.h || 1)] as Ponto;
    }) as Quadrilatero;
    return { id: p.id, distorcao: { cantos: local }, perspectiva };
  });
}

/** As linhas da grade da perspectiva (como a do Corel): `n` divisões em cada sentido, em pontos da arte. */
export function gradeDaPerspectiva(caixa: CaixaReta, cantos: Quadrilatero, n = 8): Array<[Ponto, Ponto]> {
  const m = homografiaDaCaixa(caixa, cantos);
  const linhas: Array<[Ponto, Ponto]> = [];
  for (let i = 1; i < n; i += 1) {
    const t = i / n;
    linhas.push([pelaHomografia(m, caixa.x + t * caixa.w, caixa.y), pelaHomografia(m, caixa.x + t * caixa.w, caixa.y + caixa.h)]);
    linhas.push([pelaHomografia(m, caixa.x, caixa.y + t * caixa.h), pelaHomografia(m, caixa.x + caixa.w, caixa.y + t * caixa.h)]);
  }
  return linhas;
}

export function perspectivaLimpa(bruto: unknown): PerspectivaDoGrupo | null {
  if (!bruto || typeof bruto !== "object") return null;
  const p = bruto as Record<string, unknown>;
  const c = p.caixa as Record<string, unknown> | null;
  const n = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? Math.max(-100000, Math.min(100000, v)) : NaN);
  if (!c || !Array.isArray(p.cantos) || p.cantos.length !== 4) return null;
  const caixa = { x: n(c.x), y: n(c.y), w: n(c.w), h: n(c.h) };
  const cantos = p.cantos.map((q) => (Array.isArray(q) ? [n(q[0]), n(q[1])] : [NaN, NaN]));
  if ([caixa.x, caixa.y, caixa.w, caixa.h, ...cantos.flat()].some((v) => Number.isNaN(v)) || caixa.w <= 0 || caixa.h <= 0) return null;
  return { caixa, cantos: cantos as Quadrilatero };
}
