/* ============================================================
   MapView.jsx — canvas world map (ESM, default export)
   Renders: land + graticule, ROIs, stations, night/terminator,
   satellite sine tracks, footprints, capture swaths, downlinks.
   Imperative draw(simTime, ui) called from the app RAF loop.
   ============================================================ */
import React, { useRef, useEffect } from "react";
import { geoEquirectangular, geoPath, geoGraticule, geoCircle } from "d3-geo";
import { feature } from "topojson-client";
import * as SIM from "./sim.js";
import { hexA } from "./util.js";
// Карта мира раздаётся со своего origin (Vite ?url), без рантайм-зависимости от CDN
// (см. INTEGRATION.md §3). Файл: assets/land-110m.json (world-atlas@2).
import landUrl from "./assets/land-110m.json?url";

// keep the original call-sites (d3.* / topojson.*) working unchanged
const d3 = { geoEquirectangular, geoPath, geoGraticule, geoCircle };
const topojson = { feature };

// Подспутниковая точка: реальный сэмплер env.subSat (flight-line) или встроенная
// симуляция SIM.subSat. См. INTEGRATION.md §5.2.
function subSatOf(env, sat, tMs) {
  return env.subSat ? env.subSat(sat, tMs) : SIM.subSat(sat, tMs, env.epochMs);
}

const MAP_C = {
  ocean: "#0a0e0f",
  land: "#171c1e",
  landStroke: "#252d30",
  grat: "rgba(120,150,150,0.06)",
  gratText: "rgba(150,180,178,0.25)",
  amber: "#d99a4e",
  blue: "#5b9bd5",
  link: "#6f8fae",
  red: "#d9685f",
  teal: "#5fb3a3",
  night: "rgba(20,28,46,0.55)",
  nightEdge: "rgba(90,120,170,0.18)",
  station: "#cdd6d3",
};

function MapView({ env, apiRef }) {
  const wrapRef = useRef(null);
  const canvasRef = useRef(null);
  const st = useRef({
    w: 0, h: 0, dpr: 1, proj: null, path: null,
    base: null, land: null, ready: false,
  });

  // ---- load world land (topojson via unpkg, graceful fallback) ----
  useEffect(() => {
    let alive = true;
    fetch(landUrl)
      .then((r) => r.json())
      .then((topo) => {
        if (!alive) return;
        st.current.land = topojson.feature(topo, topo.objects.land);
        buildBase();
        if (apiRef.current.onReady) apiRef.current.onReady();
      })
      .catch(() => { buildBase(); });
    return () => { alive = false; };
  }, []);

  // ---- sizing ----
  function resize() {
    const wrap = wrapRef.current, cv = canvasRef.current;
    if (!wrap || !cv) return;
    const r = wrap.getBoundingClientRect();
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    st.current.w = r.width; st.current.h = r.height; st.current.dpr = dpr;
    cv.width = r.width * dpr; cv.height = r.height * dpr;
    cv.style.width = r.width + "px"; cv.style.height = r.height + "px";
    const proj = d3.geoEquirectangular()
      .scale(r.width / (2 * Math.PI))
      .translate([r.width / 2, r.height / 2])
      .rotate([-10, 0]);
    st.current.proj = proj;
    buildBase();
  }
  useEffect(() => {
    resize();
    const ro = new ResizeObserver(resize);
    if (wrapRef.current) ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  // ---- cached base layer (land, graticule, ROIs, stations) ----
  function buildBase() {
    const s = st.current;
    if (!s.w || !s.proj) return;
    const dpr = s.dpr;
    const off = document.createElement("canvas");
    off.width = s.w * dpr; off.height = s.h * dpr;
    const ctx = off.getContext("2d");
    ctx.scale(dpr, dpr);
    const path = d3.geoPath(s.proj, ctx);

    // ocean
    ctx.fillStyle = MAP_C.ocean;
    ctx.fillRect(0, 0, s.w, s.h);

    // graticule
    const grat = d3.geoGraticule().step([30, 30]);
    ctx.beginPath(); path(grat());
    ctx.strokeStyle = MAP_C.grat; ctx.lineWidth = 1; ctx.stroke();

    // land
    if (s.land) {
      ctx.beginPath(); path(s.land);
      ctx.fillStyle = MAP_C.land; ctx.fill();
      ctx.strokeStyle = MAP_C.landStroke; ctx.lineWidth = 0.6; ctx.stroke();
    }

    // ROIs (заявки) — faint amber dashed (planar path, avoids d3 winding-complement)
    ctx.save();
    ctx.lineWidth = 1; ctx.setLineDash([4, 3]);
    for (const r of env.rois) {
      tracePoly(ctx, s.proj, r.poly);
      ctx.fillStyle = "rgba(217,154,78,0.05)"; ctx.fill();
      ctx.strokeStyle = "rgba(217,154,78,0.5)"; ctx.stroke();
    }
    ctx.restore();

    // station labels
    ctx.font = "9px 'IBM Plex Mono', monospace";
    ctx.textAlign = "left"; ctx.textBaseline = "middle";
    for (const stn of env.stations) {
      const pt = s.proj([stn.lon, stn.lat]); if (!pt) continue;
      ctx.fillStyle = "rgba(205,214,211,0.55)";
      ctx.fillText(stn.name.toUpperCase(), pt[0] + 8, pt[1] - 6);
    }

    s.base = off; s.path = path; s.ready = true;
  }

  // ---- helpers ----
  function project(lon, lat) {
    const p = st.current.proj && st.current.proj([lon, lat]);
    return p;
  }
  // draw a great-circle-ish track polyline, breaking at antimeridian wrap
  function strokeTrack(ctx, pts, color, width, alpha) {
    const s = st.current, half = s.w * 0.5;
    ctx.save(); ctx.globalAlpha = alpha; ctx.strokeStyle = color;
    ctx.lineWidth = width; ctx.lineJoin = "round"; ctx.lineCap = "round";
    ctx.beginPath();
    let prev = null, started = false;
    for (const ll of pts) {
      const p = s.proj(ll); if (!p) { started = false; continue; }
      if (started && prev && Math.abs(p[0] - prev[0]) > half) started = false;
      if (!started) { ctx.moveTo(p[0], p[1]); started = true; }
      else ctx.lineTo(p[0], p[1]);
      prev = p;
    }
    ctx.stroke(); ctx.restore();
  }

  // ---- main per-frame draw ----
  function draw(tMs, ui) {
    const s = st.current, cv = canvasRef.current;
    if (!cv || !s.proj) return;
    const ctx = cv.getContext("2d");
    ctx.setTransform(s.dpr, 0, 0, s.dpr, 0, 0);
    ctx.clearRect(0, 0, s.w, s.h);
    if (s.base) ctx.drawImage(s.base, 0, 0, s.w, s.h);

    const path = d3.geoPath(s.proj, ctx);
    const focus = ui.focusId;

    // --- night overlay (тень Земли) ---
    const ss = SIM.subSolar(tMs);
    const anti = [SIM.wrapLon(ss.lon + 180), -ss.lat];
    const nightPoly = d3.geoCircle().center(anti).radius(90)();
    ctx.beginPath(); path(nightPoly);
    ctx.fillStyle = MAP_C.night; ctx.fill();
    ctx.lineWidth = 1; ctx.strokeStyle = MAP_C.nightEdge; ctx.stroke();

    // --- per-sat dynamic ---
    const dash = (tMs / 30) % 16;
    const sats = env.sats;
    // tracks first (under glyphs)
    for (const sat of sats) {
      const dim = focus && focus !== sat.id;
      const pts = [];
      for (let dm = -52; dm <= 52; dm += 1.6) {
        pts.push([0, 0]); // placeholder replaced below
      }
      // build actual track points
      pts.length = 0;
      for (let dm = -52; dm <= 52; dm += 1.6) {
        const p = subSatOf(env, sat, tMs + dm * 60000);
        if (p) pts.push([p.lon, p.lat]);
      }
      strokeTrack(ctx, pts, sat.color, dim ? 0.8 : 1.4, dim ? 0.10 : 0.30);
    }

    // events + glyphs
    for (const sat of sats) {
      const dim = focus && focus !== sat.id;
      const stt = SIM.statusAt(sat, tMs, env);
      const p = stt.p;
      const pt = s.proj([p.lon, p.lat]); if (!pt) continue;
      const baseA = dim ? 0.25 : 1;

      // footprint circle (swath of view)
      const fp = d3.geoCircle().center([p.lon, p.lat]).radius(sat.payload === "radar" ? 6.5 : 8)();
      ctx.beginPath(); path(fp);
      ctx.fillStyle = hexA(sat.color, dim ? 0.03 : 0.055); ctx.fill();
      ctx.strokeStyle = hexA(sat.color, dim ? 0.08 : 0.16); ctx.lineWidth = 1; ctx.stroke();

      // capture (маршрут съёмки) — amber swath along track + ROI highlight
      if (stt.state === "capture") {
        const seg = [];
        for (let dm = -7; dm <= 7; dm += 1) {
          const q = subSatOf(env, sat, tMs + dm * 60000);
          if (q) seg.push([q.lon, q.lat]);
        }
        strokeTrack(ctx, seg, MAP_C.amber, dim ? 3 : 6, dim ? 0.3 : 0.85);
        ctx.setLineDash([]);
        // контур съёмки есть не у всех режимов плана — рисуем только когда задан
        if (stt.roi && stt.roi.poly && stt.roi.poly.length) {
          tracePoly(ctx, s.proj, stt.roi.poly);
          ctx.fillStyle = "rgba(217,154,78,0.16)"; ctx.fill();
          ctx.strokeStyle = MAP_C.amber; ctx.lineWidth = 1.4; ctx.stroke();
        }
      }

      // link (связь с КА) — пунктир к станции на всём окне радиовидимости (ЗРВ),
      // даже когда сброс не идёт. Тоньше/бледнее сброса; реальный сброс ляжет поверх.
      if (env.sched) {
        const lk = SIM.intervalAt((env.sched[sat.id] || {}).link, tMs);
        if (lk && lk.lon != null && lk.lat != null) {
          const lp = s.proj([lk.lon, lk.lat]);
          if (lp) {
            ctx.save();
            ctx.setLineDash([3, 6]); ctx.lineDashOffset = -dash;
            ctx.strokeStyle = hexA(MAP_C.link, dim ? 0.22 : 0.55); ctx.lineWidth = 1;
            ctx.beginPath(); ctx.moveTo(pt[0], pt[1]); ctx.lineTo(lp[0], lp[1]); ctx.stroke();
            ctx.restore();
          }
        }
      }

      // downlink (сброс) — animated dashed line to station
      if (stt.state === "downlink" && stt.station && stt.station.lon != null && stt.station.lat != null) {
        const sp = s.proj([stt.station.lon, stt.station.lat]);
        if (sp) {
          ctx.save();
          ctx.setLineDash([5, 5]); ctx.lineDashOffset = -dash;
          ctx.strokeStyle = hexA(MAP_C.blue, dim ? 0.4 : 0.95); ctx.lineWidth = 1.6;
          ctx.beginPath(); ctx.moveTo(pt[0], pt[1]); ctx.lineTo(sp[0], sp[1]); ctx.stroke();
          ctx.restore();
          // station glow
          ctx.beginPath(); ctx.arc(sp[0], sp[1], 7, 0, 7);
          ctx.fillStyle = hexA(MAP_C.blue, 0.18); ctx.fill();
        }
      }

      // stations as triangles (drawn here so glow sits under)
      // (drawn once globally below to avoid repaint per sat) -> skip

      // sat glyph
      drawSatGlyph(ctx, pt[0], pt[1], sat, stt, baseA, sat.id === focus);

      // label
      if (!dim || sat.id === focus) {
        ctx.font = "10px 'IBM Plex Mono', monospace";
        ctx.textAlign = "left"; ctx.textBaseline = "middle";
        ctx.fillStyle = hexA("#e6ebe9", baseA);
        ctx.fillText(sat.name, pt[0] + 11, pt[1] + 0.5);
      }
    }

    // stations triangles on top
    for (const stn of env.stations) {
      const sp = s.proj([stn.lon, stn.lat]); if (!sp) continue;
      drawTriangle(ctx, sp[0], sp[1], 5, MAP_C.station);
    }
  }

  function drawSatGlyph(ctx, x, y, sat, stt, alpha, isFocus) {
    const ringColor = stt.state === "capture" ? MAP_C.amber
      : stt.state === "downlink" ? MAP_C.blue
      : stt.state === "prohibit" ? MAP_C.red : null;
    // halo
    ctx.beginPath(); ctx.arc(x, y, 9, 0, 7);
    ctx.fillStyle = hexA(sat.color, 0.10 * alpha); ctx.fill();
    // ring
    if (ringColor) {
      ctx.beginPath(); ctx.arc(x, y, 7.5, 0, 7);
      ctx.strokeStyle = hexA(ringColor, alpha); ctx.lineWidth = 1.6; ctx.setLineDash([]); ctx.stroke();
    }
    if (isFocus) {
      ctx.beginPath(); ctx.arc(x, y, 11, 0, 7);
      ctx.strokeStyle = hexA(MAP_C.teal, 0.9); ctx.lineWidth = 1; ctx.stroke();
    }
    // diamond body
    ctx.save(); ctx.translate(x, y); ctx.rotate(Math.PI / 4);
    const r = 3.4;
    ctx.fillStyle = stt.eclipse ? hexA(sat.color, 0.5 * alpha) : hexA(sat.color, alpha);
    ctx.strokeStyle = hexA("#05080a", alpha); ctx.lineWidth = 1;
    ctx.beginPath(); ctx.rect(-r, -r, r * 2, r * 2); ctx.fill(); ctx.stroke();
    ctx.restore();
  }

  function drawTriangle(ctx, x, y, r, color) {
    ctx.beginPath();
    ctx.moveTo(x, y - r); ctx.lineTo(x + r, y + r * 0.8); ctx.lineTo(x - r, y + r * 0.8);
    ctx.closePath();
    ctx.fillStyle = hexA(color, 0.85); ctx.fill();
    ctx.strokeStyle = "rgba(10,14,16,0.9)"; ctx.lineWidth = 0.8; ctx.stroke();
  }

  // pointer → lon/lat + nearest sat
  function onMove(e) {
    const s = st.current, r = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - r.left, y = e.clientY - r.top;
    const ll = s.proj && s.proj.invert([x, y]);
    if (apiRef.current.onCursor) apiRef.current.onCursor(ll ? { lon: ll[0], lat: ll[1] } : null);
    // hit test sats handled by app via getHover; expose nearest
    if (apiRef.current.onPick) {
      let best = null, bd = 16;
      for (const sat of env.sats) {
        const p = subSatOf(env, sat, apiRef.current.simTime());
        const pp = p && s.proj([p.lon, p.lat]); if (!pp) continue;
        const d = Math.hypot(pp[0] - x, pp[1] - y);
        if (d < bd) { bd = d; best = sat.id; }
      }
      apiRef.current.onPick(best, x, y);
    }
  }
  function onClick() {
    if (apiRef.current.onClickPick) apiRef.current.onClickPick();
  }

  // expose imperative API
  useEffect(() => {
    apiRef.current.draw = draw;
    apiRef.current.project = project;
  });

  return (
    <div className="map-wrap" ref={wrapRef}>
      <canvas ref={canvasRef} onMouseMove={onMove} onMouseLeave={() => apiRef.current.onCursor && apiRef.current.onCursor(null)} onClick={onClick} />
    </div>
  );
}

// trace a small lon/lat polygon as a planar projected path (no antimeridian crossing)
function tracePoly(ctx, proj, poly) {
  ctx.beginPath();
  poly.forEach((pt, i) => {
    const p = proj(pt); if (!p) return;
    if (i === 0) ctx.moveTo(p[0], p[1]); else ctx.lineTo(p[0], p[1]);
  });
  ctx.closePath();
}

export default MapView;
