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

/*
 * Trocar a paleta da arte inteira num clique (o "Estilos" do Canva): as cores
 * da arte se juntam em grupos pelo tom (vermelhos com vermelhos, azuis com
 * azuis), pesados pela área que ocupam; o grupo que mais aparece vira a 1.ª
 * cor da paleta, o segundo a 2.ª, e assim por diante. Dentro do grupo, os
 * claros e escuros continuam claros e escuros (a diferença de luz vai junto),
 * então o contraste do texto com o fundo fica. Preto, branco e cinza não
 * mudam. A mesma cor vira a mesma cor em todas as páginas.
 *
 * Toda cor do documento é um "#RRGGBB" (o documento limpo só aceita assim):
 * a troca passa por todos os campos, inclusive as cores dentro dos trechos
 * de texto formatado.
 */

export type PaletaPronta = { chave: string; nome: string; cores: string[] };

/* Paletas vivas (nada de tons lavados), de 4 ou 5 cores, da mais forte para a de apoio. */
export const PALETAS_PRONTAS: PaletaPronta[] = [
  { chave: "eletrica", nome: "Elétrica", cores: ["#5B21FF", "#C8FF3D", "#FF3D8B", "#14005A"] },
  { chave: "tomate", nome: "Tomate", cores: ["#FF3B1F", "#FFF1E0", "#2B0700", "#FFB800"] },
  { chave: "oceano", nome: "Oceano", cores: ["#0057FF", "#00D1FF", "#FFE14D", "#04153D"] },
  { chave: "floresta", nome: "Floresta", cores: ["#0E7C4A", "#B7F34A", "#F6C445", "#08261A"] },
  { chave: "chiclete", nome: "Chiclete", cores: ["#FF4FB4", "#7C3AED", "#FFD23F", "#2A0E45"] },
  { chave: "por-do-sol", nome: "Pôr do sol", cores: ["#FF6B1A", "#E11D74", "#FFC93C", "#3A0B2E"] },
  { chave: "luxo", nome: "Luxo", cores: ["#0E3B2E", "#D4AF37", "#7A1E2C", "#F3EBDD"] },
  { chave: "noite", nome: "Neon da noite", cores: ["#FF2BD6", "#22D3EE", "#7C3AED", "#07051A"] },
  { chave: "cafe", nome: "Café", cores: ["#6B3A1E", "#F2A541", "#C8452D", "#2B1A10"] },
  { chave: "sorvete", nome: "Sorvete", cores: ["#FF7AB6", "#2EC4B6", "#FFC93C", "#7B61FF"] },
  { chave: "esporte", nome: "Esporte", cores: ["#E10600", "#111111", "#FFD400", "#1E40AF"] },
  { chave: "tropical", nome: "Tropical", cores: ["#00A86B", "#FF6B35", "#FFD23F", "#0B3D4F"] },
  { chave: "realeza", nome: "Realeza", cores: ["#3A0CA3", "#F72585", "#4CC9F0", "#1B0638"] },
  { chave: "terra", nome: "Terra", cores: ["#B5512F", "#E9C46A", "#2A9D8F", "#264653"] },
];

type Hsl = { h: number; s: number; l: number };

function hexParaHsl(hex: string): Hsl {
  const r = parseInt(hex.slice(1, 3), 16) / 255;
  const g = parseInt(hex.slice(3, 5), 16) / 255;
  const b = parseInt(hex.slice(5, 7), 16) / 255;
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const l = (max + min) / 2;
  const d = max - min;
  if (!d) return { h: 0, s: 0, l };
  const s = d / (1 - Math.abs(2 * l - 1));
  let h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
  h *= 60;
  if (h < 0) h += 360;
  return { h, s: Math.min(1, s), l };
}

function hslParaHex({ h, s, l }: Hsl): string {
  const c = (1 - Math.abs(2 * l - 1)) * s;
  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
  const m = l - c / 2;
  const [r, g, b] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
  const parte = (v: number) =>
    Math.round(Math.max(0, Math.min(1, v + m)) * 255)
      .toString(16)
      .padStart(2, "0");
  return `#${parte(r)}${parte(g)}${parte(b)}`.toUpperCase();
}

/* Preto, branco, cinza e os quase (o creme do papel, o grafite): pouca cor de verdade (croma), ficam como estão. */
const neutra = (c: Hsl) => c.s < 0.14 || c.s * (1 - Math.abs(2 * c.l - 1)) < 0.08;
const distanciaDoTom = (a: number, b: number) => Math.min(Math.abs(a - b), 360 - Math.abs(a - b));
const HEX = /^#[0-9A-Fa-f]{6}$/;
const HEX_NO_TEXTO = /#[0-9A-Fa-f]{6}\b/g;

/** Passa por todos os campos de um valor e troca cada cor pela função dada. */
function trocarCores<T>(valor: T, troca: (hex: string) => string): T {
  if (typeof valor === "string") {
    if (HEX.test(valor)) return troca(valor.toUpperCase()) as T;
    /* HTML dos trechos formatados: as cores do style. */
    if (valor.includes("#") && valor.includes("<")) return valor.replace(HEX_NO_TEXTO, (h) => troca(h.toUpperCase())) as T;
    return valor;
  }
  if (Array.isArray(valor)) return valor.map((v) => trocarCores(v, troca)) as T;
  if (valor && typeof valor === "object") {
    const saida: Record<string, unknown> = {};
    for (const [chave, v] of Object.entries(valor as Record<string, unknown>)) saida[chave] = chave === "src" || chave === "id" || chave === "dentroDe" || chave === "grupo" ? v : trocarCores(v, troca);
    return saida as T;
  }
  return valor;
}

/** As cores de uma peça com o peso dela (a área; texto e linha pesam menos que o que preenche). */
function pesoDaPeca(p: Peca) {
  const area = Math.max(1, p.w * p.h);
  return p.tipo === "texto" || p.tipo === "traco" || p.tipo === "icone" ? area * 0.35 : area;
}

function juntarCores(valor: unknown, peso: number, conta: Map<string, number>) {
  trocarCores(valor, (hex) => {
    conta.set(hex, (conta.get(hex) || 0) + peso);
    return hex;
  });
}

/**
 * A troca de cor que a paleta faz nessas páginas (a mesma em todas). `paleta`
 * na ordem de força; as cores neutras da paleta (branco, preto) não recebem
 * grupo, a não ser que a paleta só tenha elas.
 */
export function mapaDaPaleta(paginas: DocumentoDoBanner[], paleta: string[]): Map<string, string> {
  const conta = new Map<string, number>();
  for (const pagina of paginas) {
    juntarCores(pagina.fundo, pagina.largura * pagina.altura, conta);
    for (const p of pagina.pecas) if (!p.oculta) juntarCores(p, pesoDaPeca(p), conta);
  }
  const alvos = paleta.filter((c) => HEX.test(c)).map((c) => ({ hex: c.toUpperCase(), hsl: hexParaHsl(c) }));
  const vivas = alvos.filter((a) => !neutra(a.hsl));
  const destinos = vivas.length ? vivas : alvos;
  const mapa = new Map<string, string>();
  if (!destinos.length) return mapa;

  /* Os grupos de tom (28°), dos mais pesados para os mais leves. */
  const cores = [...conta.entries()].map(([hex, peso]) => ({ hex, peso, hsl: hexParaHsl(hex) })).filter((c) => !neutra(c.hsl));
  cores.sort((a, b) => b.peso - a.peso);
  const grupos: Array<{ h: number; peso: number; membros: typeof cores }> = [];
  for (const cor of cores) {
    const grupo = grupos.find((g) => distanciaDoTom(g.h, cor.hsl.h) <= 28);
    if (grupo) {
      grupo.membros.push(cor);
      grupo.peso += cor.peso;
    } else grupos.push({ h: cor.hsl.h, peso: cor.peso, membros: [cor] });
  }
  grupos.sort((a, b) => b.peso - a.peso);

  grupos.forEach((grupo, i) => {
    const alvo = destinos[i % destinos.length].hsl;
    /* A luz média do grupo (pesada): cada cor guarda a distância dela até a média. */
    const total = grupo.membros.reduce((t, m) => t + m.peso, 0);
    const media = grupo.membros.reduce((t, m) => t + m.hsl.l * m.peso, 0) / total;
    const saturacao = grupo.membros.reduce((t, m) => t + m.hsl.s * m.peso, 0) / total;
    for (const m of grupo.membros) {
      /* A luz anda só a metade do caminho até a da paleta: o claro continua claro e o escuro escuro (o texto não some no fundo). */
      const l = Math.max(0.04, Math.min(0.97, m.hsl.l + (alvo.l - media) * 0.5));
      const s = Math.max(0, Math.min(1, alvo.s * (saturacao > 0 ? Math.min(1.25, m.hsl.s / saturacao) : 1)));
      mapa.set(m.hex, hslParaHex({ h: alvo.h, s, l }));
    }
  });
  return mapa;
}

/* ---------------------------------------------------------- leitura */

function luminancia(hex: string) {
  const canal = (i: number) => {
    const v = parseInt(hex.slice(i, i + 2), 16) / 255;
    return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
  };
  return 0.2126 * canal(1) + 0.7152 * canal(3) + 0.0722 * canal(5);
}
export function contraste(a: string, b: string) {
  const [x, y] = [luminancia(a), luminancia(b)].sort((m, n) => n - m);
  return (x + 0.05) / (y + 0.05);
}

/* A cor que fica atrás do meio da peça: a forma mais de cima que cobre o ponto, ou o fundo da página. */
function corAtras(pagina: DocumentoDoBanner, indice: number): string | null {
  const alvo = pagina.pecas[indice];
  const cx = alvo.x + alvo.w / 2;
  const cy = alvo.y + alvo.h / 2;
  for (let i = indice - 1; i >= 0; i -= 1) {
    const p = pagina.pecas[i];
    if (p.oculta || p.opacidade < 0.6 || cx < p.x || cx > p.x + p.w || cy < p.y || cy > p.y + p.h) continue;
    if (p.tipo === "forma" && p.pintura.tipo !== "nenhuma" && (p.pintura.alfa ?? 1) > 0.6) return p.pintura.cor;
    if (p.tipo === "traco" && p.fechado && p.preenchimento.tipo !== "nenhuma") return p.preenchimento.cor;
    if (p.tipo === "imagem") return null;
  }
  return pagina.fundo.pintura.tipo === "nenhuma" ? null : pagina.fundo.pintura.cor;
}

/* A cor do texto clareada ou escurecida (o tom fica) até ler bem em cima do fundo. */
function legivel(cor: string, fundo: string, minimo: number) {
  if (contraste(cor, fundo) >= minimo) return cor;
  const hsl = hexParaHsl(cor);
  const clarear = luminancia(fundo) < 0.4;
  for (let passo = 1; passo <= 20; passo += 1) {
    const l = clarear ? hsl.l + ((0.97 - hsl.l) * passo) / 20 : hsl.l - ((hsl.l - 0.04) * passo) / 20;
    const nova = hslParaHex({ ...hsl, l });
    if (contraste(nova, fundo) >= minimo) return nova;
  }
  return clarear ? "#FFFFFF" : "#111111";
}

/* Depois da troca: texto que perdeu a leitura em cima do que está atrás ganha luz (ou sombra) até ler. */
function garantirLeitura(antes: DocumentoDoBanner, depois: DocumentoDoBanner): DocumentoDoBanner {
  const pecas = depois.pecas.map((p, i) => {
    if (p.tipo !== "texto" || p.oculta || p.pintura.tipo === "nenhuma" || p.fundo) return p;
    const fundo = corAtras(depois, i);
    const fundoAntes = corAtras(antes, i);
    const original = antes.pecas[i];
    if (!fundo || !fundoAntes || original?.tipo !== "texto") return p;
    /* Só mexe no que a troca piorou: o mínimo é o que era antes (até 4,5). */
    const minimo = Math.min(4.5, contraste(original.pintura.cor, fundoAntes));
    if (contraste(p.pintura.cor, fundo) >= minimo - 0.05) return p;
    const pintura = { ...p.pintura, cor: legivel(p.pintura.cor, fundo, minimo), cor2: legivel(p.pintura.cor2, fundo, minimo) };
    const html = p.html ? p.html.replace(HEX_NO_TEXTO, (h) => legivel(h.toUpperCase(), fundo, Math.min(minimo, 3))) : p.html;
    return { ...p, pintura, html } as Peca;
  });
  return { ...depois, pecas };
}

/** As páginas com a paleta nova (o que não tem cor no mapa fica igual) e o texto conferido para ler bem. */
export function trocarPaleta(paginas: DocumentoDoBanner[], paleta: string[]): DocumentoDoBanner[] {
  const mapa = mapaDaPaleta(paginas, paleta);
  if (!mapa.size) return paginas;
  const troca = (hex: string) => mapa.get(hex) ?? hex;
  return paginas.map((pagina) => garantirLeitura(pagina, { ...pagina, fundo: trocarCores(pagina.fundo, troca), pecas: pagina.pecas.map((p) => trocarCores(p, troca)) }));
}

/** A paleta girada (o "embaralhar": a mesma paleta com outra cor na frente). */
export const girarPaleta = (cores: string[], vezes: number) => (cores.length ? cores.map((_, i) => cores[(i + vezes) % cores.length]) : cores);
