// @wuys/map · data/bake.js
//
// Pre-render the entire "flag map of the world" into two offscreen canvases
// at init time:
//
//   • EE canvas (4096×2048, Equal Earth projection)   — used directly as an
//     SVG <image> in flat view. Pan/zoom is viewBox-only, the image moves
//     with it. Static after bake, zero per-frame cost.
//
//   • Equirectangular canvas (4096×2048, lon/lat → x/y)  — uploaded to a
//     WebGL texture wrapped on a sphere mesh in globe view. As the user
//     rotates the globe, the sphere mesh rotates; each vertex has a FIXED
//     UV, so the flag content is welded to (lat, lon). No drift, no
//     stretching, no triangle wobble.
//
// Each canvas is composited per-polygon — the same polygon ring the SVG
// renderer uses for country paths. The flag fill is clipped to the exact
// country shape, edge-for-edge. No simplification, no mesh approximation.

import * as EE from "../math/projections/equal-earth.js";
import { ringFromArcs } from "./topology.js";

const EE_W = 6144;
const EE_H = 3072;   // 2:1 — matches Equal Earth's natural viewBox aspect
const EQ_W = 6144;
const EQ_H = 3072;   // 2:1 — matches equirectangular lon/lat aspect

// EE viewBox is 2000×1000 by convention. Scale to canvas.
const EE_SX = EE_W / 2000;
const EE_SY = EE_H / 1000;

/**
 * Project (lon, lat) to EE canvas coords.
 */
function projectEE(lon, lat) {
  const [vbx, vby] = EE.project(lon, lat);
  return [vbx * EE_SX, vby * EE_SY];
}

/**
 * Project (lon, lat) to equirectangular canvas coords.
 */
function projectEQ(lon, lat) {
  const x = (lon + 180) * (EQ_W / 360);
  const y = (90 - lat)  * (EQ_H / 180);
  return [x, y];
}

/**
 * Project a ring's points and split it into sub-rings at antimeridian
 * crossings. A crossing is detected by consecutive points jumping by more
 * than half the canvas width in x (after projection both EE and equirect
 * wrap lon=±180 to opposite canvas edges).
 *
 * Returns an array of sub-rings (each is an array of [x, y] points in
 * canvas pixel coords).
 */
function projectAndSplit(ring, project, halfW) {
  const subs = [];
  let cur = [];
  let prevX = null;
  for (let i = 0; i < ring.length; i++) {
    const [x, y] = project(ring[i][0], ring[i][1]);
    if (prevX !== null && Math.abs(x - prevX) > halfW) {
      if (cur.length > 2) subs.push(cur);
      cur = [];
    }
    cur.push([x, y]);
    prevX = x;
  }
  if (cur.length > 2) subs.push(cur);
  return subs;
}

/**
 * Build a Path2D from an already-projected sub-ring (array of [x, y]).
 */
function buildPathFromSub(sub) {
  const path = new Path2D();
  path.moveTo(sub[0][0], sub[0][1]);
  for (let i = 1; i < sub.length; i++) path.lineTo(sub[i][0], sub[i][1]);
  path.closePath();
  return path;
}

/**
 * Compute axis-aligned bbox of an already-projected sub-ring.
 */
function bboxOfSub(sub) {
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (const [x, y] of sub) {
    if (x < minX) minX = x; if (x > maxX) maxX = x;
    if (y < minY) minY = y; if (y > maxY) maxY = y;
  }
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
}

/**
 * Load all flag images (data URIs from state.data.flags). Returns a Map
 * iso → HTMLImageElement that's ready to drawImage.
 */
function loadAllFlagImages(flagsMap) {
  const images = new Map();
  const promises = [];
  for (const [iso, src] of flagsMap) {
    if (!src) continue;
    promises.push(new Promise((resolve) => {
      const img = new Image();
      img.onload = () => { images.set(iso, img); resolve(); };
      img.onerror = () => resolve();
      img.decoding = "async";
      img.src = src;
    }));
  }
  return Promise.all(promises).then(() => images);
}

/**
 * Bake the flag-fills-world into BOTH canvases.
 *
 * @param {Map<string, Object>} countries  state.data.countries (iso → meta with .polys)
 * @param {Map<string, string>} flagsMap   state.data.flags (iso → flag URL/data URI)
 * @returns {Promise<{ ee: HTMLCanvasElement, eq: HTMLCanvasElement }>}
 */
// Cache the IN-FLIGHT promise (not just the result) so concurrent callers
// share the same bake. geo.js mounts the map twice on initial page load
// (setLang flow + showHub flow); both call bakeFlagMap before the first
// completes, so a result-only cache would still race.
let _bakePromise = null;

export function bakeFlagMap(countries, flagsMap, geomByIso, arcs) {
  if (_bakePromise) return _bakePromise;
  _bakePromise = _doBake(countries, flagsMap, geomByIso, arcs);
  return _bakePromise;
}

async function _doBake(countries, flagsMap, geomByIso, arcs) {
  const t0 = performance.now();
  const images = await loadAllFlagImages(flagsMap);
  const t1 = performance.now();

  function makeCanvas(w, h) {
    const c = document.createElement("canvas");
    c.width = w; c.height = h;
    const cx = c.getContext("2d");
    cx.imageSmoothingEnabled = true;
    cx.imageSmoothingQuality = "high";
    cx.clearRect(0, 0, w, h);
    return { canvas: c, cx };
  }

  const ee = makeCanvas(EE_W, EE_H);
  const eq = makeCanvas(EQ_W, EQ_H);

  let painted = 0;
  // Iterate the FULL topology — every polygon of every country, including
  // tiny islands (Norway's fjord rocks, Greek archipelago, UK Hebrides).
  // The filtered cm.polys subset is for WebGL mesh work; baking is cheap
  // per-polygon (canvas clip + drawImage) so we paint them all for pixel-
  // accurate flag coverage that matches what the SVG renderer draws.
  for (const [iso] of countries) {
    const flagImg = images.get(iso);
    if (!flagImg) continue;
    const geom = geomByIso && geomByIso.get && geomByIso.get(iso);
    if (!geom) continue;
    const polysIn = geom.type === "Polygon" ? [geom.arcs]
                : geom.type === "MultiPolygon" ? geom.arcs
                : null;
    if (!polysIn) continue;
    for (const polyArcs of polysIn) {
      if (!polyArcs.length) continue;
      // polyArcs[0] is the outer ring; polyArcs[1..] are holes (inland lakes).
      // Paint outer ring + cut holes via even-odd clip — exactly mirrors how
      // the SVG country path renders (`fill-rule="evenodd"` semantics).
      const rings = [];
      for (let r = 0; r < polyArcs.length; r++) {
        const ring = ringFromArcs(polyArcs[r], arcs);
        if (ring.length >= 3) rings.push(ring);
      }
      if (rings.length === 0) continue;
      paintPolyOntoCanvasMultiRing(ee.cx, rings, flagImg, projectEE, EE_W / 2);
      paintPolyOntoCanvasMultiRing(eq.cx, rings, flagImg, projectEQ, EQ_W / 2);
      painted++;
    }
  }
  const t2 = performance.now();
  console.log("[wuys/map/bake] images:", (t1 - t0).toFixed(0), "ms; painted",
    painted, "polys in", (t2 - t1).toFixed(0), "ms (total", (t2 - t0).toFixed(0), "ms)");

  return { ee: ee.canvas, eq: eq.canvas };
}

/**
 * Project polygon's rings (outer + holes), split at antimeridian crossings,
 * build a single Path2D with all sub-rings as subpaths, clip with even-odd
 * fill rule (outer fills, holes carve out — matches SVG path rendering).
 */
function paintPolyOntoCanvasMultiRing(cx, rings, flagImg, project, halfW) {
  const path = new Path2D();
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  let hasAny = false;
  // Outer ring (index 0) determines the bbox for flag scaling. Holes don't
  // expand bbox — flag is drawn at outer-ring extent, holes just cut out.
  const outerSubs = projectAndSplit(rings[0], project, halfW);
  for (const sub of outerSubs) {
    if (sub.length < 3) continue;
    addSubpath(path, sub);
    hasAny = true;
    for (const [x, y] of sub) {
      if (x < minX) minX = x; if (x > maxX) maxX = x;
      if (y < minY) minY = y; if (y > maxY) maxY = y;
    }
  }
  // Add hole rings.
  for (let r = 1; r < rings.length; r++) {
    const subs = projectAndSplit(rings[r], project, halfW);
    for (const sub of subs) {
      if (sub.length < 3) continue;
      addSubpath(path, sub);
    }
  }
  if (!hasAny) return;
  const w = maxX - minX, h = maxY - minY;
  if (w <= 0 || h <= 0) return;
  cx.save();
  cx.clip(path, "evenodd");
  cx.drawImage(flagImg, minX, minY, w, h);
  cx.restore();
}

function addSubpath(path, sub) {
  path.moveTo(sub[0][0], sub[0][1]);
  for (let i = 1; i < sub.length; i++) path.lineTo(sub[i][0], sub[i][1]);
  path.closePath();
}
