// @wuys/map · layers/flags.js
//
// Bake-driven flag rendering. At init we composite the entire flag-fills-
// world into TWO offscreen canvases:
//
//   • Equal Earth bake → used as an SVG <image> in flat view.
//   • Equirectangular bake → uploaded as a WebGL texture and wrapped on a
//     lat/lon sphere mesh in globe view.
//
// Both bakes use the same exact polygon rings the SVG country layer uses,
// so the flag fills are pixel-accurate to country borders — no mesh
// simplification, no over/under-extent.
//
// Per-frame work is negligible:
//   • Flat: zero. The <image> moves with SVG's viewBox.
//   • Globe: a single WebGL drawElements call on a fixed sphere mesh, with
//     an updated rotation uniform. Flag pixels are welded to (lat, lon)
//     because the sphere's UVs are fixed.

import { bakeFlagMap } from "../data/bake.js";

const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";


// FULLSCREEN-QUAD shader globe: every fragment inverse-projects itself back
// to (lat, lon) and samples the equirectangular bake at the EXACT UV. Zero
// interpolation error → flag content stays welded to the country surface
// pixel-perfect under any rotation. No sphere mesh resolution to tune.

const VERTEX_SHADER = `#version 300 es
precision highp float;
in vec2 aPos;            // clip-space quad corners (-1..+1)
out vec2 vClipPos;
void main() {
  vClipPos = aPos;
  gl_Position = vec4(aPos, 0.0, 1.0);
}`;

const FRAGMENT_SHADER = `#version 300 es
precision highp float;
in vec2 vClipPos;
out vec4 fragColor;
uniform vec2  uRotation;     // (rotLonRad, rotLatRad)
uniform float uBaseR;
uniform vec2  uViewCenter;
uniform vec4  uViewBox;      // (vbx, vby, vbw, vbh)
uniform sampler2D uTex;
const float PI = 3.14159265358979323846;
void main() {
  // 1. clip-space → viewBox-space → unit-sphere coords (px, py).
  float vbx = uViewBox.x + (vClipPos.x + 1.0) * 0.5 * uViewBox.z;
  float vby = uViewBox.y + (1.0 - vClipPos.y) * 0.5 * uViewBox.w;
  float px = (vbx - uViewCenter.x) / uBaseR;
  float py = (uViewCenter.y - vby) / uBaseR;
  float rho2 = px * px + py * py;
  if (rho2 > 1.0) discard;                  // outside the sphere disc
  float pz = sqrt(1.0 - rho2);              // front-hemisphere z

  // 2. Inverse-rotate to sphere-fixed Cartesian, then to (lat, lon).
  //    Forward (vertex side): x = cosPhi*sinLam,
  //                           y = cosPhi0*sinPhi - sinPhi0*cosPhi*cosLam,
  //                           z = sinPhi0*sinPhi + cosPhi0*cosPhi*cosLam.
  //    Inverse → lat, lon (closed-form, no iteration):
  float sinPhi0 = sin(uRotation.y);
  float cosPhi0 = cos(uRotation.y);
  float lat = asin(sinPhi0 * pz + cosPhi0 * py);
  float lon = atan(px, cosPhi0 * pz - sinPhi0 * py) + uRotation.x;
  // Wrap lon to [-PI, PI].
  if (lon >  PI) lon -= 2.0 * PI;
  if (lon < -PI) lon += 2.0 * PI;

  // 3. Equirectangular UV.
  float u = (lon + PI) / (2.0 * PI);
  float v = (PI * 0.5 - lat) / PI;
  fragColor = texture(uTex, vec2(u, v));
}`;

export default function createFlagsLayer() {
  let _svg = null;
  let _state = null;
  // Flat: a single SVG <image> element.
  let flatImage = null;
  // Globe: foreignObject containing a WebGL canvas with sphere texture.
  let globeForeign = null;
  let globeCanvas = null;
  let gl = null;
  let program = null;
  let attrLoc = null, uniLoc = null;
  let quadVbo = null;
  let texture = null;
  let lastMode = null;
  let _ready = false;

  function build(ctx) {
    const { svg, state } = ctx;
    _svg = svg;
    _state = state;

    // Insert containers up front so they appear in the right z-order; we'll
    // populate them after the async bake completes.
    flatImage = document.createElementNS(SVG_NS, "image");
    flatImage.setAttribute("x", "0");
    flatImage.setAttribute("y", "0");
    flatImage.setAttribute("width", "2000");
    flatImage.setAttribute("height", "1000");
    flatImage.setAttribute("preserveAspectRatio", "xMidYMid meet");
    flatImage.setAttribute("pointer-events", "none");
    flatImage.setAttribute("display", "none");
    svg.appendChild(flatImage);

    // Globe foreignObject is OVERSIZED — extends well past the natural EE
    // viewBox (0,0,2000,1000) so the sphere isn't clipped when zoomed in.
    // 8000×7000 covers sphere radii up to ~3500 (zoom ~7×). The canvas
    // inside fills this area; shader's uViewBox uniform matches it so
    // sphere vertices map to clip space correctly. SVG root has implicit
    // overflow:visible (default for root SVG), so foreignObject content
    // past viewBox still renders on screen.
    globeForeign = document.createElementNS(SVG_NS, "foreignObject");
    globeForeign.setAttribute("x", "-3000");
    globeForeign.setAttribute("y", "-3000");
    globeForeign.setAttribute("width", "8000");
    globeForeign.setAttribute("height", "7000");
    globeForeign.setAttribute("pointer-events", "none");
    globeForeign.setAttribute("display", "none");
    svg.appendChild(globeForeign);

    globeCanvas = document.createElementNS(XHTML_NS, "canvas");
    globeCanvas.style.cssText = "width:100%;height:100%;display:block;background:transparent;pointer-events:none;";
    globeCanvas.width = 2000;
    globeCanvas.height = 1000;
    globeForeign.appendChild(globeCanvas);

    // Kick off the bake — async because flag images need to load.
    bakeFlagMap(state.data.countries, state.data.flags, state._geomByIso, state._topologyArcs).then((bakes) => {
      // Expose bakes for offline inspection (debug only).
      try { window.__bakes = bakes; } catch (_) {}
      // Layer may have been destroyed between build start and bake completion
      // (geo.js mounts the map twice on initial load). Bail silently if so.
      if (!flatImage || !globeCanvas) return;

      try {
        flatImage.setAttribute("href", bakes.ee.toDataURL("image/png"));
      } catch (e) {
        console.warn("[wuys/map/flags] flat bake toDataURL failed:", e && e.message);
      }

      try {
        gl = globeCanvas.getContext("webgl2", { antialias: true, alpha: true, premultipliedAlpha: false });
        if (gl) setupGlobeGL(bakes.eq);
      } catch (e) {
        console.warn("[wuys/map/flags] globe gl setup failed:", e && e.message);
      }

      _ready = true;
      patch({ state: _state });
    });
  }

  function setupGlobeGL(eqCanvas) {
    program = linkProgram(gl);
    if (!program) { gl = null; return; }
    attrLoc = {
      aPos: gl.getAttribLocation(program, "aPos"),
    };
    uniLoc = {
      uRotation:   gl.getUniformLocation(program, "uRotation"),
      uBaseR:      gl.getUniformLocation(program, "uBaseR"),
      uViewCenter: gl.getUniformLocation(program, "uViewCenter"),
      uViewBox:    gl.getUniformLocation(program, "uViewBox"),
      uTex:        gl.getUniformLocation(program, "uTex"),
    };
    gl.clearColor(0, 0, 0, 0);
    gl.disable(gl.DEPTH_TEST);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

    // Fullscreen quad — two triangles covering clip space.
    quadVbo = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, quadVbo);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      -1, -1,  1, -1, -1,  1,
       1, -1,  1,  1, -1,  1,
    ]), gl.STATIC_DRAW);

    // Upload eq canvas as texture.
    // FLIP_Y=true so texture v=0 aligns with canvas y=0 (north pole).
    // Shader computes v = (PI/2 - lat) / PI → lat=90 (north) gives v=0;
    // that must sample the north-pole row of the canvas (y=0), not the
    // south-pole row (y=H). Without FLIP_Y this whole texture was flipped.
    texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, eqCanvas);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);     // lon wraps
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  }

  function patch(ctx) {
    if (!_svg) return;
    const state = (ctx && ctx.state) || _state;
    if (!state) return;
    if (!state.theme || !state.view) return;
    const showFlags = state.theme.flagsVisible === true;
    const mode = state.view.mode;
    if (mode !== lastMode) lastMode = mode;

    // GLOBE: flag rendering is intentionally disabled. The orthographic
    // projection produces visual alignment artefacts that don't justify the
    // complexity, so the globe view shows only the theme-coloured countries.
    // The flag toggle button is also hidden in globe mode (CSS via the
    // .geo-map-globe class on the grid container).
    if (mode === "globe") {
      if (flatImage) flatImage.setAttribute("display", "none");
      if (globeForeign) globeForeign.setAttribute("display", "none");
      return;
    }

    // FLAT: show / hide the baked image based on the flag toggle.
    if (!showFlags || !_ready) {
      if (flatImage) flatImage.setAttribute("display", "none");
      if (globeForeign) globeForeign.setAttribute("display", "none");
      return;
    }
    if (flatImage) flatImage.removeAttribute("display");
    if (globeForeign) globeForeign.setAttribute("display", "none");
  }

  function renderGlobe(state) {
    if (!gl || !program || !texture) return;
    // foreignObject's viewBox area is "-3000 -3000 8000 7000". Canvas inside
    // covers this same area. We size canvas pixels proportionally to its
    // screen-rendered size for sharp output, capped to avoid texture limits.
    const rect = _svg.getBoundingClientRect();
    const vb = _svg.viewBox.baseVal;
    const vbw = vb.width || 2000, vbh = vb.height || 1000;
    const dpr = window.devicePixelRatio || 1;
    const scale = Math.min(rect.width / vbw, rect.height / vbh);
    // Canvas covers 8000×7000 of viewBox-units; at 'scale' px-per-unit and
    // DPR, the pixel resolution can get large at high DPR — cap to 8192².
    let cw = Math.max(1, Math.floor(8000 * scale * dpr));
    let ch = Math.max(1, Math.floor(7000 * scale * dpr));
    if (cw > 8192) { ch = Math.floor(ch * 8192 / cw); cw = 8192; }
    if (ch > 8192) { cw = Math.floor(cw * 8192 / ch); ch = 8192; }
    if (globeCanvas.width !== cw || globeCanvas.height !== ch) {
      globeCanvas.width = cw;
      globeCanvas.height = ch;
    }
    gl.viewport(0, 0, cw, ch);

    const rotLon = state.nav.lon, rotLat = state.nav.lat, zoom = state.nav.zoom;
    const baseR = 450 * zoom;

    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.useProgram(program);
    gl.uniform2f(uniLoc.uRotation, rotLon * Math.PI / 180, rotLat * Math.PI / 180);
    gl.uniform1f(uniLoc.uBaseR, baseR);
    gl.uniform2f(uniLoc.uViewCenter, 1000.0, 500.0);
    // Shader projects (lon, lat) → viewBox coord (around 1000, 500). Then
    // viewBox coord → clip space using uViewBox which MUST match the
    // foreignObject's own viewBox area. With foreignObject at
    // (-3000, -3000, 8000, 7000), uViewBox is the same:
    gl.uniform4f(uniLoc.uViewBox, -3000, -3000, 8000, 7000);
    gl.uniform1i(uniLoc.uTex, 0);
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture);

    gl.bindBuffer(gl.ARRAY_BUFFER, quadVbo);
    gl.enableVertexAttribArray(attrLoc.aPos);
    gl.vertexAttribPointer(attrLoc.aPos, 2, gl.FLOAT, false, 8, 0);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }

  function destroy() {
    if (gl) {
      if (quadVbo) gl.deleteBuffer(quadVbo);
      if (texture) gl.deleteTexture(texture);
      if (program) gl.deleteProgram(program);
    }
    if (flatImage && flatImage.parentNode) flatImage.parentNode.removeChild(flatImage);
    if (globeForeign && globeForeign.parentNode) globeForeign.parentNode.removeChild(globeForeign);
    flatImage = null; globeForeign = null; globeCanvas = null; gl = null; program = null;
    quadVbo = null; texture = null;
  }

  return { id: "flags", build, patch, destroy };
}

// === GL helpers ==============================================================

function compileShader(gl, type, src) {
  const sh = gl.createShader(type);
  gl.shaderSource(sh, src);
  gl.compileShader(sh);
  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
    console.warn("[wuys/map/flags] shader compile failed:", gl.getShaderInfoLog(sh));
    gl.deleteShader(sh);
    return null;
  }
  return sh;
}

function linkProgram(gl) {
  const vs = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
  const fs = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
  if (!vs || !fs) return null;
  const p = gl.createProgram();
  gl.attachShader(p, vs); gl.attachShader(p, fs);
  gl.linkProgram(p);
  if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
    console.warn("[wuys/map/flags] program link failed:", gl.getProgramInfoLog(p));
    gl.deleteProgram(p);
    return null;
  }
  return p;
}
