// @wuys/map · renderers/canvas.js
//
// Canvas2D renderer. Visual parity with the SVG renderer for the core layers
// (ocean, countries, contested, flags, labels, spotlights, atmosphere). Hit-
// testing uses an offscreen id-color buffer (each country gets a unique RGB tag
// drawn into a hidden canvas; pointer lookup samples a single pixel).
//
// Why both Canvas and SVG?
//   • SVG  — best at low-medium country counts; DOM-based; vector-crisp.
//   • Canvas — best at high counts (no DOM cost); easier path to a WebGL2 rewrite;
//              cheaper to composite atmospheric effects.
// User can pick at runtime via the renderer dropdown in the overlay UI.

import * as EE    from "../math/projections/equal-earth.js";
import * as ORTHO from "../math/projections/orthographic.js";
import { splitOnAntimeridian } from "../math/antimeridian.js";
import { clipToHorizon }       from "../math/horizon.js";
import { ringFromArcs }        from "../data/topology.js";
import { registerRenderer }    from "./base.js";

const VB_W = 2000, VB_H = 1000;

export function createCanvasRenderer({ state, events, theme } = {}) {
  let target = null;
  let canvas = null, ctx2d = null;
  let hitCanvas = null, hitCtx = null;          // offscreen id-buffer
  let dpr = 1;
  let arcs = null, geomByIso = null;
  /** Map<iso, HTMLImageElement> — preloaded flag images. */
  const flagImages = new Map();
  /** Map<iso, [r,g,b]> — unique color tag per country for hit-test. */
  const idColors = new Map();
  /** Reverse lookup: "r,g,b" string → iso */
  const colorToIso = new Map();

  function init(targetEl) {
    target = targetEl;
    canvas = document.createElement("canvas");
    canvas.style.cssText = "width:100%;height:100%;display:block;";
    target.appendChild(canvas);
    ctx2d = canvas.getContext("2d");
    hitCanvas = document.createElement("canvas");
    hitCtx = hitCanvas.getContext("2d", { willReadFrequently: true });
    arcs = state._topologyArcs;
    geomByIso = state._geomByIso;
    assignIdColors();
    preloadFlags();
    resize();
    window.addEventListener("resize", resize);
  }

  function resize() {
    if (!target) return;
    const rect = target.getBoundingClientRect();
    dpr = window.devicePixelRatio || 1;
    canvas.width  = Math.max(1, Math.floor(rect.width  * dpr));
    canvas.height = Math.max(1, Math.floor(rect.height * dpr));
    hitCanvas.width  = canvas.width;
    hitCanvas.height = canvas.height;
  }

  function assignIdColors() {
    let i = 0;
    for (const iso of state.data.countries.keys()) {
      i++;
      const r = (i >> 16) & 0xff;
      const g = (i >> 8)  & 0xff;
      const b =  i        & 0xff;
      idColors.set(iso, [r, g, b]);
      colorToIso.set(r + "," + g + "," + b, iso);
    }
  }

  function preloadFlags() {
    for (const [iso, src] of state.data.flags) {
      if (!src) continue;
      const img = new Image();
      img.src = src;
      img.onload = () => requestPatch();
      flagImages.set(iso, img);
    }
  }

  /** Coalesce successive patch() calls into one rAF. */
  let pendingPatch = false;
  function requestPatch() {
    if (pendingPatch) return;
    pendingPatch = true;
    requestAnimationFrame(() => { pendingPatch = false; patch(); });
  }

  // ===========================================================================
  // Per-frame draw
  // ===========================================================================

  function patch() {
    if (!ctx2d) return;
    const w = canvas.width, h = canvas.height;
    const scale = Math.min(w / VB_W, h / VB_H);
    const ox = (w - VB_W * scale) / 2;
    const oy = (h - VB_H * scale) / 2;

    ctx2d.save();
    ctx2d.clearRect(0, 0, w, h);
    // Background (matches target.style.background in SVG)
    ctx2d.fillStyle = "#050210";
    ctx2d.fillRect(0, 0, w, h);
    ctx2d.translate(ox, oy);
    ctx2d.scale(scale, scale);

    const mode = state.view.mode;
    const rotLambdaRad = state.nav.lon * Math.PI / 180;
    const rotPhiRad    = state.nav.lat * Math.PI / 180;
    const sinRotPhi = Math.sin(rotPhiRad), cosRotPhi = Math.cos(rotPhiRad);
    const orthoProject = ORTHO.projector(rotLambdaRad, rotPhiRad);
    const baseR = 450 * state.nav.zoom;
    const CX = 1000, CY = 500;

    // Flat-mode pan via translation (matches SVG's viewBox shift).
    if (mode === "flat") {
      const z = state.nav.zoom;
      const [vbx, vby] = EE.project(state.nav.lon, state.nav.lat);
      const w2 = VB_W / z, h2 = VB_H / z;
      const cx = Math.max(w2 / 2, Math.min(VB_W - w2 / 2, vbx));
      const cy = Math.max(h2 / 2, Math.min(VB_H - h2 / 2, vby));
      // Map viewBox sub-rect → fill the same scaled viewBox space.
      ctx2d.scale(z, z);
      ctx2d.translate(-(cx - w2 / 2), -(cy - h2 / 2));
    }

    // ---- Ocean ---------------------------------------------------------------
    if (mode === "flat") {
      ctx2d.fillStyle = theme.ocean.fill;
      ctx2d.fillRect(0, 0, VB_W, VB_H);
    } else {
      ctx2d.beginPath();
      ctx2d.arc(CX, CY, baseR, 0, Math.PI * 2);
      ctx2d.fillStyle = theme.ocean.fill;
      ctx2d.fill();
      if (theme.ocean.strokeWidth) {
        ctx2d.lineWidth = theme.ocean.strokeWidth;
        ctx2d.strokeStyle = theme.ocean.stroke || "transparent";
        ctx2d.stroke();
      }
    }

    function eachCountryRings(geom, cb) {
      const polys = geom.type === "Polygon" ? [geom.arcs]
                 : geom.type === "MultiPolygon" ? geom.arcs : null;
      if (!polys) return;
      for (const poly of polys) for (const ring of poly) cb(ring);
    }

    function isVisible(cm) {
      if (mode !== "globe") return true;
      const phi = cm.lat * Math.PI / 180;
      const lam = cm.lon * Math.PI / 180;
      const cosDist = Math.sin(phi) * sinRotPhi + Math.cos(phi) * cosRotPhi * Math.cos(lam - rotLambdaRad);
      return cosDist >= -0.3;
    }

    // ---- Country layer -------------------------------------------------------
    const flagsOn = state.theme.flagsVisible && !theme.countries.hidesFlags;
    ctx2d.lineWidth = theme.countries.strokeWidth || 0.5;
    ctx2d.strokeStyle = theme.countries.stroke || "rgba(255,255,255,0.35)";
    for (const [iso, geom] of geomByIso) {
      const cm = state.data.countries.get(iso);
      if (!cm || !isVisible(cm)) continue;
      const path2d = buildCountryPath2D(iso, geom, mode, orthoProject, baseR, CX, CY);
      if (!path2d) continue;
      // Flag fill via clip + drawImage, or solid fallback.
      const flag = flagsOn ? flagImages.get(iso) : null;
      if (flag && flag.complete && flag.naturalWidth > 0) {
        const rect = countryRectFor(cm, mode, orthoProject, baseR, CX, CY);
        if (rect) {
          ctx2d.save();
          ctx2d.clip(path2d);
          ctx2d.drawImage(flag, rect.x, rect.y, rect.w, rect.h);
          ctx2d.restore();
        }
      } else {
        ctx2d.fillStyle = theme.countries.fillFallback || "#1f1430";
        ctx2d.fill(path2d);
      }
      ctx2d.stroke(path2d);
    }

    // ---- Contested regions ---------------------------------------------------
    if (state.data.contestedRegions && state.layers.enabled.has("contested")) {
      ctx2d.save();
      ctx2d.lineWidth = theme.contested.strokeWidth || 1.0;
      ctx2d.fillStyle = theme.contested.fill;
      ctx2d.strokeStyle = theme.contested.stroke;
      if (theme.contested.strokeDasharray) {
        const dash = String(theme.contested.strokeDasharray).split(/\s+/).map(Number);
        ctx2d.setLineDash(dash);
      }
      for (const r of state.data.contestedRegions) {
        const path = buildRingPath2D(r.points, mode, orthoProject, baseR, CX, CY);
        if (path) { ctx2d.fill(path); ctx2d.stroke(path); }
      }
      ctx2d.restore();
    }

    // ---- Labels --------------------------------------------------------------
    if (state.layers.enabled.has("labels") && (!state.theme.flagsVisible || theme.countries.showsLabels)) {
      const f = theme.labels;
      ctx2d.font = (f.fontStyle || "normal") + " " + (f.fontWeight || 400) + " 16px " + (f.fontFamily || "serif");
      ctx2d.fillStyle = f.fill;
      ctx2d.strokeStyle = f.stroke || "transparent";
      ctx2d.lineWidth = f.strokeWidth || 0;
      ctx2d.textAlign = "center";
      ctx2d.textBaseline = "middle";
      for (const [iso, cm] of state.data.countries) {
        if (!isVisible(cm)) continue;
        const lbl = state.data.labels.get(iso) || null;
        const lon = lbl?.lon ?? cm.lon;
        const lat = lbl?.lat ?? cm.lat;
        const proj = projectPoint(lon, lat, mode, orthoProject, baseR, CX, CY);
        if (!proj) continue;
        const projH = mode === "flat" ? cm.latRange * 5.555 : cm.latRange * baseR / 57.3;
        if (projH < 10) continue;
        const sizeRatio = lbl?.sizeRatio || autoSizeRatio(cm);
        const fontSize = Math.max(5, Math.min(60, sizeRatio * projH));
        const name = cm.name || iso.toUpperCase();
        ctx2d.font = (f.fontStyle || "normal") + " " + (f.fontWeight || 400) + " " + fontSize.toFixed(1) + "px " + (f.fontFamily || "serif");
        if (lbl?.rotate) {
          ctx2d.save();
          ctx2d.translate(proj[0], proj[1]);
          ctx2d.rotate(lbl.rotate * Math.PI / 180);
          if (ctx2d.lineWidth > 0) ctx2d.strokeText(name, 0, 0);
          ctx2d.fillText(name, 0, 0);
          ctx2d.restore();
        } else {
          if (ctx2d.lineWidth > 0) ctx2d.strokeText(name, proj[0], proj[1]);
          ctx2d.fillText(name, proj[0], proj[1]);
        }
      }
    }

    // ---- Spotlights ----------------------------------------------------------
    if (state.layers.enabled.has("spotlights") && state.data.spotlights) {
      const sp = theme.spotlights;
      const zoom = state.nav.zoom;
      const fadeStart = 3.0, fadeEnd = 5.5;
      let alpha = 1;
      if (zoom > fadeStart) alpha = Math.max(0, 1 - (zoom - fadeStart) / (fadeEnd - fadeStart));
      if (alpha > 0.001) {
        const dotR = Math.max(1.4, 3.0 / Math.max(0.7, Math.pow(zoom, 0.55)));
        const haloR = dotR * 3.2;
        ctx2d.globalAlpha = alpha;
        for (const s of state.data.spotlights) {
          const proj = projectPoint(s.lon, s.lat, mode, orthoProject, baseR, CX, CY);
          if (!proj) continue;
          // Halo
          ctx2d.beginPath();
          ctx2d.arc(proj[0], proj[1], haloR, 0, Math.PI * 2);
          ctx2d.strokeStyle = sp.glow;
          ctx2d.lineWidth = 1.5;
          ctx2d.globalAlpha = alpha * 0.6;
          ctx2d.stroke();
          // Dot
          ctx2d.globalAlpha = alpha;
          ctx2d.beginPath();
          ctx2d.arc(proj[0], proj[1], dotR, 0, Math.PI * 2);
          ctx2d.fillStyle = s.special ? (sp.specialFill || sp.fill) : sp.fill;
          ctx2d.fill();
          ctx2d.strokeStyle = s.special ? sp.specialBorder : sp.border;
          ctx2d.lineWidth = 1;
          ctx2d.stroke();
        }
        ctx2d.globalAlpha = 1;
      }
    }

    // ---- Atmosphere (globe only) --------------------------------------------
    if (mode === "globe" && state.layers.enabled.has("atmosphere")) {
      const atmCol = theme.globe?.atmosphere?.color || "rgba(110,180,255,0.4)";
      const glowR = baseR * 1.16;
      const gradGlow = ctx2d.createRadialGradient(CX, CY, baseR * 0.8, CX, CY, glowR);
      gradGlow.addColorStop(0,   "rgba(0,0,0,0)");
      gradGlow.addColorStop(0.5, atmCol);
      gradGlow.addColorStop(1,   "rgba(0,0,0,0)");
      ctx2d.beginPath();
      ctx2d.arc(CX, CY, glowR, 0, Math.PI * 2);
      ctx2d.fillStyle = gradGlow;
      ctx2d.fill();

      // Inner day/night shading
      const sunDx = -baseR * 0.35, sunDy = -baseR * 0.35;
      const gradShade = ctx2d.createRadialGradient(CX + sunDx, CY + sunDy, 0, CX + sunDx, CY + sunDy, baseR);
      gradShade.addColorStop(0,    "rgba(255,255,255,0)");
      gradShade.addColorStop(0.55, "rgba(0,0,0,0)");
      gradShade.addColorStop(1,    "rgba(0,0,0,0.55)");
      ctx2d.save();
      ctx2d.beginPath();
      ctx2d.arc(CX, CY, baseR, 0, Math.PI * 2);
      ctx2d.clip();
      ctx2d.fillStyle = gradShade;
      ctx2d.fillRect(0, 0, VB_W, VB_H);
      ctx2d.restore();
    }

    ctx2d.restore();
    // Repaint id-buffer for hit-testing.
    paintIdBuffer(orthoProject, baseR, CX, CY, mode);
  }

  // ---- Hit-test buffer -------------------------------------------------------

  function paintIdBuffer(orthoProject, baseR, CX, CY, mode) {
    const w = hitCanvas.width, h = hitCanvas.height;
    const scale = Math.min(w / VB_W, h / VB_H);
    const ox = (w - VB_W * scale) / 2, oy = (h - VB_H * scale) / 2;
    hitCtx.save();
    hitCtx.clearRect(0, 0, w, h);
    hitCtx.translate(ox, oy);
    hitCtx.scale(scale, scale);
    if (mode === "flat") {
      const z = state.nav.zoom;
      const [vbx, vby] = EE.project(state.nav.lon, state.nav.lat);
      const w2 = VB_W / z, h2 = VB_H / z;
      const cx = Math.max(w2 / 2, Math.min(VB_W - w2 / 2, vbx));
      const cy = Math.max(h2 / 2, Math.min(VB_H - h2 / 2, vby));
      hitCtx.scale(z, z);
      hitCtx.translate(-(cx - w2 / 2), -(cy - h2 / 2));
    }
    for (const [iso, geom] of geomByIso) {
      const cm = state.data.countries.get(iso);
      if (!cm) continue;
      // Same visibility check as the main draw.
      if (mode === "globe") {
        const phi = cm.lat * Math.PI / 180;
        const lam = cm.lon * Math.PI / 180;
        const rotPhiRad = state.nav.lat * Math.PI / 180;
        const rotLambdaRad = state.nav.lon * Math.PI / 180;
        const cosDist = Math.sin(phi) * Math.sin(rotPhiRad) + Math.cos(phi) * Math.cos(rotPhiRad) * Math.cos(lam - rotLambdaRad);
        if (cosDist < -0.3) continue;
      }
      const path = buildCountryPath2D(iso, geom, mode, orthoProject, baseR, CX, CY);
      if (!path) continue;
      const [r, g, b] = idColors.get(iso);
      hitCtx.fillStyle = "rgb(" + r + "," + g + "," + b + ")";
      hitCtx.fill(path);
    }
    hitCtx.restore();
  }

  function hitTest(clientX, clientY) {
    if (!canvas || !hitCtx) return null;
    const rect = canvas.getBoundingClientRect();
    const x = Math.floor((clientX - rect.left) * (hitCanvas.width / rect.width));
    const y = Math.floor((clientY - rect.top)  * (hitCanvas.height / rect.height));
    if (x < 0 || y < 0 || x >= hitCanvas.width || y >= hitCanvas.height) return null;
    const px = hitCtx.getImageData(x, y, 1, 1).data;
    const key = px[0] + "," + px[1] + "," + px[2];
    const iso = colorToIso.get(key);
    return iso ? { iso } : null;
  }

  // ---- Path builders ---------------------------------------------------------

  function buildCountryPath2D(iso, geom, mode, orthoProject, baseR, CX, CY) {
    const polys = geom.type === "Polygon" ? [geom.arcs]
               : geom.type === "MultiPolygon" ? geom.arcs : null;
    if (!polys) return null;
    let dStr = "";
    for (const poly of polys) {
      for (const ring of poly) {
        const pts = ringFromArcs(ring, arcs);
        if (pts.length < 3) continue;
        if (mode === "flat") {
          const proj = pts.map(([lon, lat]) => EE.project(lon, lat));
          dStr += splitOnAntimeridian(proj, 1000);
        } else {
          const projected = pts.map(([lon, lat]) => orthoProject(lon, lat));
          dStr += clipToHorizon(projected, (p) => [CX + p.x * baseR, CY - p.y * baseR]);
        }
      }
    }
    if (!dStr) return null;
    return new Path2D(dStr);
  }

  function buildRingPath2D(points, mode, orthoProject, baseR, CX, CY) {
    if (!points || points.length < 3) return null;
    let dStr = "";
    if (mode === "flat") {
      const proj = points.map(([lon, lat]) => EE.project(lon, lat));
      dStr = splitOnAntimeridian(proj, 1000);
    } else {
      const projected = points.map(([lon, lat]) => orthoProject(lon, lat));
      dStr = clipToHorizon(projected, (p) => [CX + p.x * baseR, CY - p.y * baseR]);
    }
    return dStr ? new Path2D(dStr) : null;
  }

  function countryRectFor(cm, mode, orthoProject, baseR, CX, CY) {
    // Use the main-body bbox so flag fills track the country's largest sub-polygon
    // (USA continental, RU mainland) rather than spanning to distant exclaves.
    const bMinLon = cm.mainLonMin ?? cm.lonMin;
    const bMaxLon = cm.mainLonMax ?? cm.lonMax;
    const bMinLat = cm.mainLatMin ?? cm.latMin;
    const bMaxLat = cm.mainLatMax ?? cm.latMax;
    const bLonRange = cm.mainLonRange ?? cm.lonRange;
    const bLatRange = cm.mainLatRange ?? cm.latRange;
    if (mode === "flat") {
      const ul = EE.project(bMinLon, bMaxLat);
      const lr = EE.project(bMaxLon, bMinLat);
      return { x: ul[0], y: ul[1], w: lr[0] - ul[0], h: lr[1] - ul[1] };
    }
    const pc = orthoProject(cm.lon, cm.lat);
    if (pc.z < -0.3) return null;
    const cosLat = Math.cos(cm.lat * Math.PI / 180);
    const w = 1.2 * Math.max(8, bLonRange * cosLat) * baseR / 57.3;
    const h = 1.2 * Math.max(8, bLatRange) * baseR / 57.3;
    const cx = CX + pc.x * baseR, cy = CY - pc.y * baseR;
    return { x: cx - w / 2, y: cy - h / 2, w, h };
  }

  function projectPoint(lon, lat, mode, orthoProject, baseR, CX, CY) {
    if (mode === "flat") {
      const [x, y] = EE.project(lon, lat);
      return [x, y];
    }
    const p = orthoProject(lon, lat);
    if (p.z < 0) return null;
    return [CX + p.x * baseR, CY - p.y * baseR];
  }

  function autoSizeRatio(cm) {
    const aspect = (cm.lonRange || 30) / (cm.latRange || 30);
    if (aspect > 3) return 0.18;
    if (aspect < 0.4) return 0.14;
    return 0.22;
  }

  // ---- Renderer contract -----------------------------------------------------

  function worldToScreen({ lon, lat }) {
    const rect = canvas.getBoundingClientRect();
    let vbx, vby;
    if (state.view.mode === "flat") {
      [vbx, vby] = EE.project(lon, lat);
      const z = state.nav.zoom;
      const [cvbx, cvby] = EE.project(state.nav.lon, state.nav.lat);
      const w2 = VB_W / z, h2 = VB_H / z;
      const cx = Math.max(w2 / 2, Math.min(VB_W - w2 / 2, cvbx));
      const cy = Math.max(h2 / 2, Math.min(VB_H - h2 / 2, cvby));
      const left = cx - w2 / 2, top = cy - h2 / 2;
      const scale = Math.min(rect.width / w2, rect.height / h2);
      const ox = (rect.width - w2 * scale) / 2, oy = (rect.height - h2 * scale) / 2;
      return { x: ox + (vbx - left) * scale, y: oy + (vby - top) * scale };
    }
    const p = ORTHO.project(lon, lat, { rotLon: state.nav.lon, rotLat: state.nav.lat, zoom: state.nav.zoom });
    const scale = Math.min(rect.width / VB_W, rect.height / VB_H);
    const ox = (rect.width - VB_W * scale) / 2, oy = (rect.height - VB_H * scale) / 2;
    return { x: ox + p.vbx * scale, y: oy + p.vby * scale };
  }

  function screenToWorld({ x, y }) {
    const rect = canvas.getBoundingClientRect();
    if (state.view.mode === "flat") {
      const z = state.nav.zoom;
      const [cvbx, cvby] = EE.project(state.nav.lon, state.nav.lat);
      const w2 = VB_W / z, h2 = VB_H / z;
      const cx = Math.max(w2 / 2, Math.min(VB_W - w2 / 2, cvbx));
      const cy = Math.max(h2 / 2, Math.min(VB_H - h2 / 2, cvby));
      const left = cx - w2 / 2, top = cy - h2 / 2;
      const scale = Math.min(rect.width / w2, rect.height / h2);
      const ox = (rect.width - w2 * scale) / 2, oy = (rect.height - h2 * scale) / 2;
      const vbx = left + (x - ox) / scale;
      const vby = top  + (y - oy) / scale;
      return EE.unproject(vbx, vby);
    }
    const scale = Math.min(rect.width / VB_W, rect.height / VB_H);
    const ox = (rect.width - VB_W * scale) / 2, oy = (rect.height - VB_H * scale) / 2;
    const vbx = (x - ox) / scale, vby = (y - oy) / scale;
    return ORTHO.unproject(vbx, vby, { rotLon: state.nav.lon, rotLat: state.nav.lat, zoom: state.nav.zoom });
  }

  function viewportToWorld(dx, dy) {
    const c = worldToScreen({ lon: state.nav.lon, lat: state.nav.lat });
    const t = screenToWorld({ x: c.x + dx, y: c.y + dy });
    return { dLon: t.lon - state.nav.lon, dLat: t.lat - state.nav.lat };
  }

  function getViewport() {
    return { lonMin: -180, lonMax: 180, latMin: -90, latMax: 90 };
  }

  function destroy() {
    window.removeEventListener("resize", resize);
    if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
    canvas = null; ctx2d = null; hitCanvas = null; hitCtx = null;
    flagImages.clear(); idColors.clear(); colorToIso.clear();
  }

  function applyTheme() { requestPatch(); }

  return {
    init: (t) => Promise.resolve(init(t)),
    patch, destroy, hitTest,
    worldToScreen, screenToWorld, viewportToWorld, getViewport,
    applyTheme,
    getCanvas: () => canvas,
  };
}

registerRenderer("canvas", (args) => createCanvasRenderer(args));
