/*
 * Tirar o fundo de uma foto no próprio navegador (nada vai para servidor de
 * terceiros, sem custo por uso). O modelo é o IS-Net (isnet-general-use, do
 * DIS, licença Apache 2.0, uso comercial livre), quantizado em 8 bits pela
 * Ktalogu (~49 MB, as 40 últimas camadas inteiras para não sujar o fundo) e
 * guardado no depósito público. Ele baixa uma vez e fica no cache do
 * navegador. A conta roda num Worker (a tela não trava) com o onnxruntime-web
 * (MIT) só de wasm, servido pelo jsDelivr, em até 4 núcleos quando a página é
 * isolada (o KtaloguDesign é, ver proxy.ts): ~2x mais rápido que com um só.
 *
 * Antes do modelo, a conta pela cor: logo e arte com fundo de uma cor só
 * (fundo-liso.ts) saem limpas sem a névoa que o modelo deixa nelas, na
 * hora e sem baixar nada. Foto (fundo com luz, degradê ou de várias cores)
 * vai para o modelo, como antes.
 */
import { analisarFundoLiso, tirarFundoLiso } from "./fundo-liso";

const ORT = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/";
const MODELO = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/products/editor-modelos/ia/isnet-general-use-ktalogu-v1.onnx`;
const CACHE = "ktalogu-tirar-fundo-v1";
const LADO = 1024;
/* A foto recortada sai com no máximo isto no lado maior. */
const MAXIMO = 2400;

export type EtapaDoFundo = { etapa: "baixando"; progresso: number } | { etapa: "tirando" };

/*
 * O Worker: "preparar" cria a sessão (com o modelo e os núcleos), "rodar"
 * faz a conta. Vários núcleos só quando a página é isolada (o KtaloguDesign é,
 * ver proxy.ts); se a sessão com vários não abrir, tenta com um.
 */
const CODIGO_DO_WORKER = `
let sessao = null;
async function criar(modelo, nucleos) {
  if (typeof ort === "undefined") {
    importScripts(${JSON.stringify(`${ORT}ort.wasm.min.js`)});
    ort.env.wasm.wasmPaths = ${JSON.stringify(ORT)};
  }
  const bytes = new Uint8Array(modelo);
  try {
    ort.env.wasm.numThreads = nucleos;
    sessao = await ort.InferenceSession.create(bytes, { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
  } catch (erro) {
    if (nucleos <= 1) throw erro;
    ort.env.wasm.numThreads = 1;
    sessao = await ort.InferenceSession.create(bytes, { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
  }
  return ort.env.wasm.numThreads;
}
self.onmessage = async (e) => {
  const m = e.data;
  try {
    if (m.tipo === "preparar") {
      const nucleos = sessao ? ort.env.wasm.numThreads : await criar(m.modelo, m.nucleos);
      self.postMessage({ ok: true, nucleos });
      return;
    }
    if (!sessao) throw new Error("sem-sessao");
    const entrada = new ort.Tensor("float32", m.dados, [1, 3, ${LADO}, ${LADO}]);
    const saida = await sessao.run({ [sessao.inputNames[0]]: entrada });
    const mascara = new Float32Array(saida[sessao.outputNames[0]].data.subarray(0, ${LADO * LADO}));
    self.postMessage({ ok: true, mascara }, [mascara.buffer]);
  } catch (erro) {
    self.postMessage({ ok: false, erro: String((erro && erro.message) || erro) });
  }
};`;

let trabalhador: Worker | null = null;
/* A sessão pronta no Worker (criada uma vez; a próxima foto já vai direto). */
let preparo: Promise<number> | null = null;

/** Quantos núcleos a conta pode usar aqui (vários só na página isolada). */
function nucleosLiberados() {
  return typeof window !== "undefined" && window.crossOriginIsolated ? Math.max(1, Math.min(4, navigator.hardwareConcurrency || 4)) : 1;
}

function pedir<T>(mensagem: Record<string, unknown>, transferir: Transferable[]): Promise<T> {
  if (!trabalhador) trabalhador = new Worker(URL.createObjectURL(new Blob([CODIGO_DO_WORKER], { type: "text/javascript" })));
  const w = trabalhador;
  return new Promise((resolver, rejeitar) => {
    w.onmessage = (e: MessageEvent<{ ok: boolean; erro?: string } & T>) => (e.data.ok ? resolver(e.data) : rejeitar(new Error(e.data.erro || "falhou")));
    w.onerror = () => rejeitar(new Error("O navegador não conseguiu rodar a ferramenta de tirar o fundo."));
    w.postMessage(mensagem, transferir);
  });
}

/** Cria a sessão no Worker (baixa o modelo se precisar). Uma vez só: as próximas esperam a mesma. */
function preparar(aoAndar: (e: EtapaDoFundo) => void) {
  preparo ??= modelo(aoAndar)
    .then((bytes) => pedir<{ nucleos: number }>({ tipo: "preparar", modelo: bytes, nucleos: nucleosLiberados() }, [bytes]))
    .then((r) => r.nucleos)
    .catch((erro) => {
      preparo = null;
      trabalhador?.terminate();
      trabalhador = null;
      throw erro;
    });
  return preparo;
}

/**
 * Deixa a ferramenta pronta antes do clique (quando a pessoa escolhe uma
 * foto), mas só se o modelo já estiver no cache: nunca baixa 49 MB sozinho.
 */
export async function aquecerTirarFundo() {
  if (preparo || typeof caches === "undefined") return;
  const guardado = await caches
    .open(CACHE)
    .then((c) => c.match(MODELO))
    .catch(() => undefined);
  if (guardado) preparar(() => undefined).catch(() => undefined);
}

export class FotoDeOutroSite extends Error {}

/** O modelo: do cache do navegador ou baixado agora (com o progresso). */
async function modelo(aoAndar: (e: EtapaDoFundo) => void): Promise<ArrayBuffer> {
  const cache = await caches.open(CACHE).catch(() => null);
  const guardado = await cache?.match(MODELO).catch(() => undefined);
  if (guardado) return guardado.arrayBuffer();
  aoAndar({ etapa: "baixando", progresso: 0 });
  const resposta = await fetch(MODELO);
  if (!resposta.ok || !resposta.body) throw new Error("Não deu para baixar a ferramenta de tirar o fundo. Confira a internet.");
  const total = Number(resposta.headers.get("content-length")) || 49_500_000;
  const leitor = resposta.body.getReader();
  const pedacos: Uint8Array[] = [];
  let lidos = 0;
  for (;;) {
    const { done, value } = await leitor.read();
    if (done) break;
    pedacos.push(value);
    lidos += value.length;
    aoAndar({ etapa: "baixando", progresso: Math.min(0.99, lidos / total) });
  }
  const bytes = new Uint8Array(lidos);
  let posicao = 0;
  for (const p of pedacos) {
    bytes.set(p, posicao);
    posicao += p.length;
  }
  await cache?.put(MODELO, new Response(bytes.slice(0), { headers: { "content-type": "application/octet-stream" } })).catch(() => undefined);
  return bytes.buffer;
}

async function carregarFoto(src: string) {
  const img = new Image();
  img.crossOrigin = "anonymous";
  img.src = src;
  try {
    await img.decode();
  } catch {
    throw new FotoDeOutroSite("Não deu para abrir essa foto (ela vem de outro site). Baixe a foto e envie pelo botão Enviar imagem.");
  }
  return img;
}

/**
 * Tira o fundo da foto e devolve o arquivo novo (WebP ou PNG com transparência),
 * no tamanho da foto (até 2400 px no lado maior).
 */
/** O corte da foto sem fundo: onde o objeto está na foto original (fração 0 a 1 da largura e da altura). */
export type CorteDoFundo = { x0: number; y0: number; x1: number; y1: number };

/**
 * `visivel`: a parte da foto que aparece na peça (fração), para o corte não
 * trazer de volta o que a moldura escondia. Devolve o arquivo já aparado no
 * objeto (sem a sobra transparente) e onde esse corte fica na foto.
 */
export async function tirarFundo(src: string, aoAndar: (e: EtapaDoFundo) => void, visivel: CorteDoFundo = { x0: 0, y0: 0, x1: 1, y1: 1 }): Promise<{ arquivo: File; corte: CorteDoFundo; pelaCor: boolean }> {
  /* As marcas de tempo (performance) mostram onde o tempo vai: foto, preparo, conta, montagem. */
  performance.mark("fundo:inicio");
  const img = await carregarFoto(src);
  const escala = Math.min(1, MAXIMO / Math.max(img.naturalWidth, img.naturalHeight));
  const largura = Math.max(1, Math.round(img.naturalWidth * escala));
  const altura = Math.max(1, Math.round(img.naturalHeight * escala));

  /* Fundo de uma cor só (logo, arte): sai pela cor, sem o modelo. */
  const final = document.createElement("canvas");
  final.width = largura;
  final.height = altura;
  const f = final.getContext("2d", { willReadFrequently: true })!;
  f.drawImage(img, 0, 0, largura, altura);
  let pontos: ImageData;
  try {
    pontos = f.getImageData(0, 0, largura, altura);
  } catch {
    throw new FotoDeOutroSite("Essa foto vem de outro site e o navegador não deixa mexer nela. Baixe a foto e envie pelo botão Enviar imagem.");
  }
  const liso = analisarFundoLiso(pontos.data, largura, altura);
  const limpo = liso ? tirarFundoLiso(pontos.data, largura, altura, liso) : null;
  if (limpo) {
    aoAndar({ etapa: "tirando" });
    pontos.data.set(limpo);
    f.putImageData(pontos, 0, 0);
    performance.mark("fundo:conta-feita");
    return { ...(await aparada(final, largura, altura, visivel)), pelaCor: true };
  }

  /* A entrada do modelo: a foto esticada em 1024 × 1024, (v/255 − 0,5), canais separados. */
  const quadrado = document.createElement("canvas");
  quadrado.width = LADO;
  quadrado.height = LADO;
  const q = quadrado.getContext("2d", { willReadFrequently: true })!;
  q.drawImage(img, 0, 0, LADO, LADO);
  let pixels: Uint8ClampedArray;
  try {
    pixels = q.getImageData(0, 0, LADO, LADO).data;
  } catch {
    throw new FotoDeOutroSite("Essa foto vem de outro site e o navegador não deixa mexer nela. Baixe a foto e envie pelo botão Enviar imagem.");
  }
  const area = LADO * LADO;
  const dados = new Float32Array(3 * area);
  for (let i = 0; i < area; i++) {
    dados[i] = pixels[i * 4] / 255 - 0.5;
    dados[area + i] = pixels[i * 4 + 1] / 255 - 0.5;
    dados[2 * area + i] = pixels[i * 4 + 2] / 255 - 0.5;
  }

  let saida: Float32Array;
  try {
    performance.mark("fundo:foto-pronta");
    await preparar(aoAndar);
    performance.mark("fundo:sessao-pronta");
    aoAndar({ etapa: "tirando" });
    saida = (await pedir<{ mascara: Float32Array }>({ tipo: "rodar", dados }, [dados.buffer])).mascara;
    performance.mark("fundo:conta-feita");
  } catch (erro) {
    /* O Worker morreu (memória): na próxima tentativa começa do zero. */
    trabalhador?.terminate();
    trabalhador = null;
    preparo = null;
    /* Os avisos daqui seguem como estão; erro técnico do motor vira um aviso comum. */
    throw erro instanceof Error && /^(Não deu|O navegador|Essa foto)/.test(erro.message) ? erro : new Error("Não deu para tirar o fundo agora. Tente de novo.");
  }

  /* A máscara (normalizada 0..1) vira o alfa da foto. */
  let menor = Infinity;
  let maior = -Infinity;
  for (let i = 0; i < area; i++) {
    if (saida[i] < menor) menor = saida[i];
    if (saida[i] > maior) maior = saida[i];
  }
  const faixa = Math.max(1e-6, maior - menor);
  const mascara = new ImageData(LADO, LADO);
  for (let i = 0; i < area; i++) mascara.data[i * 4 + 3] = Math.round(((saida[i] - menor) / faixa) * 255);
  q.clearRect(0, 0, LADO, LADO);
  q.putImageData(mascara, 0, 0);

  f.globalCompositeOperation = "destination-in";
  f.imageSmoothingQuality = "high";
  f.drawImage(quadrado, 0, 0, largura, altura);
  return { ...(await aparada(final, largura, altura, visivel)), pelaCor: false };
}

/* O arquivo sem fundo, aparado no objeto (sem a sobra transparente), e onde esse corte fica na foto. */
async function aparada(final: HTMLCanvasElement, largura: number, altura: number, visivel: CorteDoFundo): Promise<{ arquivo: File; corte: CorteDoFundo }> {
  const f = final.getContext("2d")!;
  /* Apara a sobra: a menor caixa com o objeto (alfa acima de 8), dentro da parte visível, com 2 px de folga. */
  const alfaFinal = f.getImageData(0, 0, largura, altura).data;
  const [vx0, vy0, vx1, vy1] = [Math.floor(visivel.x0 * largura), Math.floor(visivel.y0 * altura), Math.ceil(visivel.x1 * largura), Math.ceil(visivel.y1 * altura)].map((v, i) => Math.max(0, Math.min(i % 2 ? altura : largura, v)));
  let [x0, y0, x1, y1] = [Infinity, Infinity, -Infinity, -Infinity];
  for (let y = vy0; y < vy1; y++) {
    const linha = y * largura * 4;
    for (let x = vx0; x < vx1; x++) {
      if (alfaFinal[linha + x * 4 + 3] > 8) {
        if (x < x0) x0 = x;
        if (x > x1) x1 = x;
        if (y < y0) y0 = y;
        if (y > y1) y1 = y;
      }
    }
  }
  if (!Number.isFinite(x0)) throw new Error("Não achei nenhum objeto nesta foto para separar do fundo.");
  x0 = Math.max(vx0, x0 - 2);
  y0 = Math.max(vy0, y0 - 2);
  x1 = Math.min(vx1, x1 + 3);
  y1 = Math.min(vy1, y1 + 3);
  const aparada = document.createElement("canvas");
  aparada.width = x1 - x0;
  aparada.height = y1 - y0;
  aparada.getContext("2d")!.drawImage(final, x0, y0, x1 - x0, y1 - y0, 0, 0, x1 - x0, y1 - y0);

  const blob = await new Promise<Blob | null>((r) => aparada.toBlob(r, "image/webp", 0.9));
  /* Navegador sem WebP com transparência: PNG. */
  const arquivo = blob && blob.type === "image/webp" ? blob : await new Promise<Blob | null>((r) => aparada.toBlob(r, "image/png"));
  if (!arquivo) throw new Error("Não deu para montar a foto sem fundo.");
  const tipo = arquivo.type === "image/webp" ? "webp" : "png";
  performance.mark("fundo:montada");
  return { arquivo: new File([arquivo], `sem-fundo.${tipo}`, { type: arquivo.type }), corte: { x0: x0 / largura, y0: y0 / altura, x1: x1 / largura, y1: y1 / altura } };
}

/**
 * Onde a foto aparece na caixa da peça (em px da caixa): o tamanho dela em
 * tela e o deslocamento, para "conter" e "cobrir" (com o enquadre).
 */
export function fotoNaCaixa(p: { w: number; h: number; ajuste: "cobrir" | "conter"; enquadre?: { zoom: number; x: number; y: number } | null }, natural: { w: number; h: number }) {
  const zoom = p.ajuste === "cobrir" ? Math.max(1, p.enquadre?.zoom || 1) : 1;
  const s = (p.ajuste === "cobrir" ? Math.max(p.w / natural.w, p.h / natural.h) : Math.min(p.w / natural.w, p.h / natural.h)) * zoom;
  const fx = p.ajuste === "cobrir" && p.enquadre ? p.enquadre.x / 100 : 0.5;
  const fy = p.ajuste === "cobrir" && p.enquadre ? p.enquadre.y / 100 : 0.5;
  /* O zoom do enquadre cresce em volta do ponto escolhido (transform-origin no mesmo ponto). */
  const largura = natural.w * s;
  const altura = natural.h * s;
  const base = { x: (p.w - natural.w * (s / zoom)) * fx, y: (p.h - natural.h * (s / zoom)) * fy };
  const origem = { x: p.w * fx, y: p.h * fy };
  return { x: origem.x + (base.x - origem.x) * zoom, y: origem.y + (base.y - origem.y) * zoom, w: largura, h: altura };
}
