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

/*
 * Aviso de CMYK para a gráfica: a tela mostra cores (RGB) que a tinta da
 * impressão (CMYK) não alcança, como verde-limão, azul elétrico e rosa neon;
 * impressas, saem apagadas. A conta: a cor no espaço Lab (luz, croma e tom) e
 * o croma máximo aproximado que a impressão comum (FOGRA39, papel couché)
 * consegue em cada tom. Passou do limite, avisa e sugere a cor mais próxima
 * que imprime (o mesmo tom e a mesma luz, com o croma no limite).
 */

type Lab = { L: number; a: number; b: number };

function hexParaRgb(hex: string): [number, number, number] | null {
  const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
  if (!m) return null;
  const v = parseInt(m[1], 16);
  return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
}

const linear = (c: number) => {
  const v = c / 255;
  return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
};
const gama = (v: number) => Math.round(Math.max(0, Math.min(1, v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055)) * 255);
const BRANCO = [0.95047, 1, 1.08883];

function rgbParaLab([r, g, b]: [number, number, number]): Lab {
  const [R, G, B] = [linear(r), linear(g), linear(b)];
  const xyz = [R * 0.4124 + G * 0.3576 + B * 0.1805, R * 0.2126 + G * 0.7152 + B * 0.0722, R * 0.0193 + G * 0.1192 + B * 0.9505];
  const f = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);
  const [fx, fy, fz] = xyz.map((v, i) => f(v / BRANCO[i]));
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
}

function labParaHex({ L, a, b }: Lab) {
  const fy = (L + 16) / 116;
  const fx = fy + a / 500;
  const fz = fy - b / 200;
  const inv = (t: number) => (t ** 3 > 216 / 24389 ? t ** 3 : (116 * t - 16) / (24389 / 27));
  const [X, Y, Z] = [inv(fx) * BRANCO[0], inv(fy) * BRANCO[1], inv(fz) * BRANCO[2]];
  const r = X * 3.2406 + Y * -1.5372 + Z * -0.4986;
  const g = X * -0.9689 + Y * 1.8758 + Z * 0.0415;
  const bb = X * 0.0557 + Y * -0.204 + Z * 1.057;
  return `#${[gama(r), gama(g), gama(bb)].map((v) => v.toString(16).padStart(2, "0")).join("")}`.toUpperCase();
}

/* O croma máximo aproximado da impressão por tom (graus), entre os pontos medidos. */
const LIMITE: Array<[number, number]> = [
  [0, 70],
  [30, 76],
  [45, 78],
  [70, 80],
  [90, 90],
  [105, 94],
  [125, 78],
  [150, 66],
  [180, 50],
  [210, 44],
  [240, 46],
  [270, 56],
  [300, 62],
  [330, 70],
  [360, 70],
];

function cromaMaximo(tom: number, L: number) {
  const h = ((tom % 360) + 360) % 360;
  let max = 70;
  for (let i = 1; i < LIMITE.length; i++) {
    const [h0, c0] = LIMITE[i - 1];
    const [h1, c1] = LIMITE[i];
    if (h <= h1) {
      max = c0 + ((c1 - c0) * (h - h0)) / (h1 - h0);
      break;
    }
  }
  /* Muito clara ou muito escura, a tinta tem menos croma (exceto o amarelo, que é claro). */
  const extremo = L > 88 && !(h > 75 && h < 115) ? (100 - L) / 12 : L < 22 ? L / 22 : 1;
  return max * Math.max(0.25, Math.min(1, extremo));
}

export type CorForaDoCmyk = { cor: string; sugestao: string; perda: number };

/** A cor imprime como na tela? Se não, a sugestão (o mesmo tom e luz, com o croma no limite). */
export function avaliarCor(hex: string): CorForaDoCmyk | null {
  const rgb = hexParaRgb(hex);
  if (!rgb) return null;
  const lab = rgbParaLab(rgb);
  const croma = Math.hypot(lab.a, lab.b);
  const tom = (Math.atan2(lab.b, lab.a) * 180) / Math.PI;
  const limite = cromaMaximo(tom, lab.L);
  if (croma <= limite * 1.08) return null;
  const k = (limite * 0.97) / croma;
  return { cor: hex.toUpperCase(), sugestao: labParaHex({ L: lab.L, a: lab.a * k, b: lab.b * k }), perda: Math.round((1 - k) * 100) };
}

/** As cores da arte que não imprimem bem (sem repetir), da mais fora para a menos. */
export function coresForaDoCmyk(paginas: DocumentoDoBanner[]): CorForaDoCmyk[] {
  const vistas = new Set<string>();
  for (const pagina of paginas) {
    /* Toda cor guardada no documento é "#RRGGBB": procura no documento inteiro (pintura, borda, texto, sombra...). */
    for (const m of JSON.stringify({ fundo: pagina.fundo, pecas: pagina.pecas.filter((p) => !p.oculta) }).matchAll(/"#([0-9a-fA-F]{6})"/g)) vistas.add(`#${m[1].toUpperCase()}`);
  }
  return [...vistas]
    .map(avaliarCor)
    .filter((c): c is CorForaDoCmyk => Boolean(c))
    .sort((a, b) => b.perda - a.perda);
}

/** Troca as cores na arte (todas as páginas), pelas sugeridas. */
export function trocarCores<T>(dados: T, mapa: Record<string, string>): T {
  const texto = JSON.stringify(dados).replace(/"#([0-9a-fA-F]{6})"/g, (tudo, hex: string) => {
    const nova = mapa[`#${hex.toUpperCase()}`];
    return nova ? `"${nova}"` : tudo;
  });
  return JSON.parse(texto) as T;
}
