import { PERSPECTIVA, type Caixa, type EstadoNaCena } from "./animacao";

/*
 * O 3D da animação (Virar, Tombar, Balançar em 3D, a transição Virar): o
 * palco usa o CSS (perspective + rotateX + rotateY, em cssDoEstado) e o MP4
 * desenha a camada com a MESMA conta aqui — os quatro cantos projetados e o
 * WebGL pintando a imagem neles com a perspectiva certa na textura (o canvas
 * 2D só sabe torcer em paralelogramo).
 */

/** A distância da câmera (px da arte) para a caixa (a mesma no CSS e no MP4). */
export const perspectivaDa = (caixa: Pick<Caixa, "w" | "h">) => PERSPECTIVA * Math.max(caixa.w, caixa.h, 1);

export const tem3D = (e: Pick<EstadoNaCena, "rx" | "ry">) => Math.abs(e.rx) > 0.01 || Math.abs(e.ry) > 0.01;

/**
 * O ponto (px da camada) depois do estado, como o CSS faz com
 * `translate(dx,dy) rotate(giro) scale(escala) perspective(P) rotateX(rx) rotateY(ry)`
 * em volta de `centro`: [x, y, w], com `w` o divisor da perspectiva.
 */
export function pontoNoEstado(e: EstadoNaCena, centro: [number, number], p: number, x: number, y: number): [number, number, number] {
  const [ry, rx, g] = [(e.ry * Math.PI) / 180, (e.rx * Math.PI) / 180, (e.giro * Math.PI) / 180];
  const qx = x - centro[0];
  const qy = y - centro[1];
  /* rotateY e depois rotateX (o CSS aplica da direita para a esquerda). */
  const x1 = qx * Math.cos(ry);
  const z1 = -qx * Math.sin(ry);
  const y2 = qy * Math.cos(rx) - z1 * Math.sin(rx);
  const z2 = qy * Math.sin(rx) + z1 * Math.cos(rx);
  const w = Math.max(0.05, 1 - z2 / p);
  const [px, py] = [x1 / w, y2 / w];
  return [centro[0] + e.dx + e.escala * (px * Math.cos(g) - py * Math.sin(g)), centro[1] + e.dy + e.escala * (px * Math.sin(g) + py * Math.cos(g)), w];
}

/* ------------------------------------------------------------ WebGL */

type Gl = { canvas: OffscreenCanvas; gl: WebGLRenderingContext; programa: WebGLProgram; posicao: WebGLBuffer; textura: WebGLTexture };
let pronto: Gl | null = null;

function montarGl(largura: number, altura: number): Gl | null {
  if (typeof OffscreenCanvas === "undefined") return null;
  if (pronto && pronto.canvas.width === largura && pronto.canvas.height === altura) return pronto;
  const canvas = pronto?.canvas ?? new OffscreenCanvas(largura, altura);
  canvas.width = largura;
  canvas.height = altura;
  if (pronto) return (pronto = { ...pronto, canvas });
  const gl = canvas.getContext("webgl", { premultipliedAlpha: true, preserveDrawingBuffer: true, antialias: true }) as WebGLRenderingContext | null;
  if (!gl) return null;
  const sombreador = (tipo: number, codigo: string) => {
    const s = gl.createShader(tipo)!;
    gl.shaderSource(s, codigo);
    gl.compileShader(s);
    return s;
  };
  const programa = gl.createProgram()!;
  gl.attachShader(programa, sombreador(gl.VERTEX_SHADER, "attribute vec4 p;attribute vec2 uv;varying vec2 v;void main(){v=uv;gl_Position=p;}"));
  gl.attachShader(programa, sombreador(gl.FRAGMENT_SHADER, "precision mediump float;varying vec2 v;uniform sampler2D img;uniform float alfa;void main(){gl_FragColor=texture2D(img,v)*alfa;}"));
  gl.linkProgram(programa);
  const posicao = gl.createBuffer()!;
  const textura = gl.createTexture()!;
  gl.bindTexture(gl.TEXTURE_2D, textura);
  for (const [k, v] of [
    [gl.TEXTURE_MIN_FILTER, gl.LINEAR],
    [gl.TEXTURE_MAG_FILTER, gl.LINEAR],
    [gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE],
    [gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE],
  ])
    gl.texParameteri(gl.TEXTURE_2D, k, v);
  pronto = { canvas, gl, programa, posicao, textura };
  return pronto;
}

/**
 * Desenha `imagem` (ocupando a camada largura × altura) no estado 3D, em
 * `ctx` (no espaço da camada). Sem WebGL, cai no desenho chapado (sem o 3D).
 */
export function desenharEm3D(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, imagem: CanvasImageSource, e: EstadoNaCena, centro: [number, number], caixa: Caixa, escala: number, largura: number, altura: number, alfa: number): boolean {
  const W = Math.round(largura);
  const H = Math.round(altura);
  const g = montarGl(W, H);
  if (!g) return false;
  const { gl, programa } = g;
  const p = perspectivaDa(caixa) * escala;
  const c: [number, number] = [centro[0] * escala, centro[1] * escala];
  const estado = { ...e, dx: e.dx * escala, dy: e.dy * escala };
  /* Uma malha 8 × 8 (a perspectiva fica certa na textura e o canto que chega perto da câmera não estoura). */
  const N = 8;
  const dados: number[] = [];
  const vertice = (i: number, j: number) => {
    const [x, y, w] = pontoNoEstado(estado, c, p, (i / N) * W, (j / N) * H);
    dados.push(((x / W) * 2 - 1) * w, (1 - (y / H) * 2) * w, 0, w, i / N, j / N);
  };
  for (let j = 0; j < N; j += 1)
    for (let i = 0; i < N; i += 1) {
      vertice(i, j);
      vertice(i + 1, j);
      vertice(i, j + 1);
      vertice(i + 1, j);
      vertice(i + 1, j + 1);
      vertice(i, j + 1);
    }
  gl.viewport(0, 0, W, H);
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.useProgram(programa);
  gl.bindBuffer(gl.ARRAY_BUFFER, g.posicao);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(dados), gl.DYNAMIC_DRAW);
  const ap = gl.getAttribLocation(programa, "p");
  const auv = gl.getAttribLocation(programa, "uv");
  gl.enableVertexAttribArray(ap);
  gl.vertexAttribPointer(ap, 4, gl.FLOAT, false, 24, 0);
  gl.enableVertexAttribArray(auv);
  gl.vertexAttribPointer(auv, 2, gl.FLOAT, false, 24, 16);
  gl.bindTexture(gl.TEXTURE_2D, g.textura);
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imagem as TexImageSource);
  gl.uniform1f(gl.getUniformLocation(programa, "alfa"), Math.max(0, Math.min(1, alfa)));
  gl.disable(gl.BLEND);
  gl.drawArrays(gl.TRIANGLES, 0, N * N * 6);
  ctx.drawImage(g.canvas, 0, 0, largura, altura);
  return true;
}

/** O CSS do 3D (vai no fim do transform do estado, com a mesma perspectiva). */
export const css3D = (e: Pick<EstadoNaCena, "rx" | "ry">, caixa: Pick<Caixa, "w" | "h">) => (tem3D(e) ? ` perspective(${perspectivaDa(caixa).toFixed(0)}px) rotateX(${e.rx.toFixed(2)}deg) rotateY(${e.ry.toFixed(2)}deg)` : "");
