/*
 * Tirar o fundo de cor lisa (logo, arte, print: o fundo é uma cor só e o
 * desenho tem poucas cores). O modelo de IA de tirar fundo é feito para foto
 * (gente, produto): numa logo ele deixa uma névoa da cor do fundo na borda e
 * dentro das letras. Aqui a conta é pela cor:
 *
 * 1. A cor do fundo sai da borda da imagem (a mediana). Se a borda não é de
 *    uma cor só, ou se o desenho tem muitas cores (foto de produto no fundo
 *    branco, que tem sombra e brilho), não é aqui: vale o modelo.
 * 2. Cada ponto é fundo (perto da cor do fundo), desenho (longe dela e longe
 *    da borda do desenho) ou borda (o meio-termo do antisserrilhado).
 * 3. Na borda, o ponto é uma mistura da cor do desenho ao lado com a do
 *    fundo: a transparência é quanto ele tem de desenho, e a cor volta a ser
 *    a do desenho, sem o fundo misturado (é o que tirava a névoa).
 *
 * Os buracos das letras (o "o", o "e") têm a cor do fundo e saem também.
 * Roda sem DOM: RGBA de 0 a 255, sem pré-multiplicar.
 */

export type FundoLiso = { cor: [number, number, number]; ruido: number };

const distancia = (d: Uint8ClampedArray | Uint8Array, i: number, c: readonly number[]) => {
  const r = d[i] - c[0];
  const g = d[i + 1] - c[1];
  const b = d[i + 2] - c[2];
  return Math.sqrt(r * r + g * g + b * b);
};

const mediana = (valores: number[]) => {
  const ordenados = [...valores].sort((a, b) => a - b);
  return ordenados[Math.floor(ordenados.length / 2)] ?? 0;
};

/**
 * O fundo é de cor lisa? Devolve a cor e o ruído dela (o JPG espalha a cor),
 * ou null quando é foto (borda de várias cores, transparente ou desenho com
 * cores demais).
 */
export function analisarFundoLiso(dados: Uint8ClampedArray | Uint8Array, largura: number, altura: number): FundoLiso | null {
  if (largura < 8 || altura < 8) return null;
  const faixa = Math.max(2, Math.round(Math.min(largura, altura) * 0.01));
  const borda: number[] = [];
  let transparentes = 0;
  let total = 0;
  const olhar = (x: number, y: number) => {
    const i = (y * largura + x) * 4;
    total += 1;
    if (dados[i + 3] < 128) transparentes += 1;
    else borda.push(i);
  };
  for (let y = 0; y < altura; y++) {
    if (y < faixa || y >= altura - faixa) {
      for (let x = 0; x < largura; x++) olhar(x, y);
    } else {
      for (let x = 0; x < faixa; x++) {
        olhar(x, y);
        olhar(largura - 1 - x, y);
      }
    }
  }
  /* PNG que já veio com transparência: não há cor de fundo para tirar. */
  if (transparentes > total * 0.4 || borda.length < 16) return null;
  const cor: [number, number, number] = [mediana(borda.map((i) => dados[i])), mediana(borda.map((i) => dados[i + 1])), mediana(borda.map((i) => dados[i + 2]))];
  const distancias = borda.map((i) => distancia(dados, i, cor)).sort((a, b) => a - b);
  /* A borda precisa ser quase toda da mesma cor (o desenho pode encostar numa ponta). */
  const perto = distancias.filter((d) => d < 40).length / distancias.length;
  if (perto < 0.9) return null;
  const ruido = Math.max(10, distancias[Math.floor(distancias.length * 0.9)] * 1.4 + 6);
  /*
   * Fundo liso de verdade varia pouco (logo em JPG fica por volta de 12).
   * Degradê, luz de estúdio e sombra (foto, mockup) passam disso: lá a cor
   * do fundo também aparece dentro do objeto (o branco da etiqueta), e só o
   * modelo sabe o que é fundo.
   */
  if (ruido > 26) return null;

  /*
   * Desenho de poucas cores? Logo e arte ficam quase todas em poucas cores
   * (fora a borda das letras); foto de produto se espalha em sombra e brilho.
   * A conta é numa amostra (no máximo ~250 mil pontos).
   */
  const passo = Math.max(1, Math.floor(Math.sqrt((largura * altura) / 250_000)));
  const cores = new Map<number, number>();
  let doDesenho = 0;
  for (let y = 0; y < altura; y += passo) {
    for (let x = 0; x < largura; x += passo) {
      const i = (y * largura + x) * 4;
      if (dados[i + 3] < 128 || distancia(dados, i, cor) <= ruido * 2) continue;
      doDesenho += 1;
      const chave = ((dados[i] >> 4) << 8) | ((dados[i + 1] >> 4) << 4) | (dados[i + 2] >> 4);
      cores.set(chave, (cores.get(chave) || 0) + 1);
    }
  }
  /* Quase nada além do fundo: imagem vazia, não tem o que separar. */
  if (doDesenho < 20) return null;
  const principais = [...cores.values()].sort((a, b) => b - a).slice(0, 8).reduce((s, n) => s + n, 0);
  if (principais / doDesenho < 0.72) return null;
  return { cor, ruido };
}

/**
 * Tira o fundo de cor lisa: devolve os pontos novos (RGBA), com a
 * transparência na borda do desenho e a cor dele sem o fundo misturado; null
 * quando a conta pela cor não serve (comeria o desenho).
 */
export function tirarFundoLiso(dados: Uint8ClampedArray | Uint8Array, largura: number, altura: number, fundo: FundoLiso): Uint8ClampedArray | null {
  const n = largura * altura;
  const { cor, ruido } = fundo;
  /*
   * Pela distância até a cor do fundo: fundo (no ruído dele), forte (bem
   * longe) ou meio-termo. O desenho "cheio" é o forte que não tem fundo nem
   * meio-termo a 2 pontos: a rampa do antisserrilhado tem 1 a 3 pontos e o
   * chiado do JPG mora nela; tudo o que fica nessa faixa é borda (mistura).
   */
  const forte = Math.max(ruido * 1.8, ruido + 25, 40);
  const dist = new Float32Array(n);
  const fraco = new Uint8Array(n);
  for (let p = 0; p < n; p++) {
    const i = p * 4;
    const d = dados[i + 3] < 8 ? 0 : distancia(dados, i, cor);
    dist[p] = d;
    if (d < forte) fraco[p] = 1;
  }
  /* "Tem fraco a até 2 pontos": duas passadas (deitada e em pé) da janela de 5. */
  const deitada = new Uint8Array(n);
  for (let y = 0; y < altura; y++) {
    const linha = y * largura;
    for (let x = 0; x < largura; x++) {
      let tem = 0;
      for (let dx = -2; dx <= 2 && !tem; dx++) {
        const xx = x + dx;
        if (xx >= 0 && xx < largura) tem = fraco[linha + xx];
      }
      deitada[linha + x] = tem;
    }
  }
  const cheio = new Uint8Array(n);
  for (let y = 0; y < altura; y++) {
    for (let x = 0; x < largura; x++) {
      const p = y * largura + x;
      if (fraco[p]) continue;
      let tem = 0;
      for (let dy = -2; dy <= 2 && !tem; dy++) {
        const yy = y + dy;
        if (yy >= 0 && yy < altura) tem = deitada[yy * largura + x];
      }
      if (!tem) cheio[p] = 1;
    }
  }

  const saida = new Uint8ClampedArray(n * 4);
  const desenho = [0, 0, 0];
  for (let y = 0; y < altura; y++) {
    for (let x = 0; x < largura; x++) {
      const p = y * largura + x;
      const i = p * 4;
      if (dist[p] <= ruido) continue;
      if (cheio[p]) {
        saida[i] = dados[i];
        saida[i + 1] = dados[i + 1];
        saida[i + 2] = dados[i + 2];
        saida[i + 3] = dados[i + 3];
        continue;
      }
      /* Borda: a cor do desenho ao lado (a média do cheio a até 4 pontos). */
      let r = 0;
      let g = 0;
      let b = 0;
      let quantos = 0;
      for (let dy = -4; dy <= 4; dy++) {
        const yy = y + dy;
        if (yy < 0 || yy >= altura) continue;
        for (let dx = -4; dx <= 4; dx++) {
          const xx = x + dx;
          if (xx < 0 || xx >= largura) continue;
          const q = yy * largura + xx;
          if (cheio[q]) {
            const j = q * 4;
            r += dados[j];
            g += dados[j + 1];
            b += dados[j + 2];
            quantos += 1;
          }
        }
      }
      if (quantos) {
        desenho[0] = r / quantos;
        desenho[1] = g / quantos;
        desenho[2] = b / quantos;
      } else {
        /* Sem cheio perto: traço fino (o ponto mais forte a até 2, se for forte mesmo) ou chiado do fundo, que some. */
        let maior = -1;
        let j = i;
        for (let dy = -2; dy <= 2; dy++) {
          const yy = y + dy;
          if (yy < 0 || yy >= altura) continue;
          for (let dx = -2; dx <= 2; dx++) {
            const xx = x + dx;
            if (xx < 0 || xx >= largura) continue;
            const q = yy * largura + xx;
            if (dist[q] > maior) {
              maior = dist[q];
              j = q * 4;
            }
          }
        }
        if (maior < forte) continue;
        desenho[0] = dados[j];
        desenho[1] = dados[j + 1];
        desenho[2] = dados[j + 2];
      }
      /* Quanto do ponto é desenho: a projeção dele na reta fundo → desenho. */
      const vx = desenho[0] - cor[0];
      const vy = desenho[1] - cor[1];
      const vz = desenho[2] - cor[2];
      const comprimento = vx * vx + vy * vy + vz * vz;
      let alfa = comprimento > 1 ? ((dados[i] - cor[0]) * vx + (dados[i + 1] - cor[1]) * vy + (dados[i + 2] - cor[2]) * vz) / comprimento : 1;
      /* Perto do ruído do fundo, some aos poucos (o resto do JPG não vira névoa). */
      alfa = Math.max(0, Math.min(1, alfa)) * Math.min(1, ((dist[p] - ruido) / Math.max(1, ruido)) * 2);
      if (alfa <= 0.03) continue;
      /* A cor sem o fundo: a do ponto "desmisturada", indo para a do desenho quando quase transparente. */
      const mistura = Math.min(1, alfa * 1.6);
      for (let c = 0; c < 3; c++) {
        const limpa = cor[c] + (dados[i + c] - cor[c]) / Math.max(alfa, 0.05);
        saida[i + c] = Math.round(Math.max(0, Math.min(255, limpa * mistura + desenho[c] * (1 - mistura))));
      }
      saida[i + 3] = Math.round(alfa * dados[i + 3]);
    }
  }
  /*
   * A trava: o que ficou de pé tem que ser quase tudo o que estava bem longe
   * da cor do fundo. Se a conta comeu o desenho (uma foto que parecia lisa),
   * null: quem chamou usa o modelo de IA.
   */
  let esperado = 0;
  let ficou = 0;
  for (let p = 0; p < n; p++) {
    if (dist[p] <= forte) continue;
    esperado += 1;
    if (saida[p * 4 + 3] > 128) ficou += 1;
  }
  return esperado && ficou / esperado < 0.85 ? null : saida;
}
