/* global d3, topojson, React */
const PMT_FILL = {
  status: { state: "#2B57B5", cftc: "#767676", unresolved: "#9EBDFF", none: "#DEDEDE", nonec: "#DEDEDE" },
  trade: { full: "#2B57B5", events: "#9EBDFF", blocked: "#767676" }
};
const PMT_DARK = new Set(["#2B57B5", "#0068FE", "#767676"]);
const CALLOUT = ["Vermont", "New Hampshire", "Massachusetts", "Rhode Island", "Connecticut", "New Jersey", "Delaware", "Maryland", "District of Columbia"];

function UsMap({ view, platform, selected, onSelect }) {  const hostRef = React.useRef(null);
  const apiRef = React.useRef(null);
  const fitRef = React.useRef(null);
  const [tip, setTip] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const cbRef = React.useRef(onSelect);
  cbRef.current = onSelect;

  React.useEffect(() => {
    let dead = false;
    (async () => {
      try {
        const topo = await d3.json(((window.__resources || {}).usAtlas) || "https://cdn.jsdelivr.net/npm/us-atlas@3.0.1/states-10m.json");
        if (dead) return;
        const byName = {};
        window.PMT.states.forEach((s) => (byName[s.name] = s));
        const geo = topojson.feature(topo, topo.objects.states);
        const feats = geo.features.filter((f) => byName[f.properties.name]);
        const svg = d3.select(hostRef.current).append("svg")
          .attr("viewBox", "0 0 960 520").attr("width", "100%").attr("role", "img")
          .style("height", "auto")
          .attr("aria-label", "United States, states coloured by regulatory status");
        const proj = d3.geoAlbersUsa().fitExtent([[10, 6], [862, 514]], { type: "FeatureCollection", features: feats });
        const path = d3.geoPath(proj);

        const gStates = svg.append("g");
        const gCircuit = svg.append("g").attr("pointer-events", "none");
        const gCall = svg.append("g").attr("class", "pmt-callouts");
        const gLabel = svg.append("g").attr("pointer-events", "none").attr("class", "pmt-statelabels");

        const paths = gStates.selectAll("path").data(feats).join("path")
          .attr("d", path).attr("stroke", "#FFFFFF").attr("stroke-width", 0.7).attr("stroke-linejoin", "round")
          .attr("tabindex", 0).attr("role", "button")
          .style("cursor", "pointer").style("transition", "fill 140ms cubic-bezier(0.2,0,0,1)")
          .on("click", (e, f) => cbRef.current(byName[f.properties.name].abbr))
          .on("keydown", (e, f) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); cbRef.current(byName[f.properties.name].abbr); } })
          .on("mousemove", (e, f) => {
            const r = hostRef.current.getBoundingClientRect();
            setTip({ x: e.clientX - r.left, y: e.clientY - r.top, abbr: byName[f.properties.name].abbr });
          })
          .on("mouseleave", () => setTip(null));

        // Abbreviation labels where the state is wide enough to hold one.
        const labels = feats.map((f) => {
          const b = path.bounds(f), c = proj(d3.geoCentroid(f));
          return { f, s: byName[f.properties.name], w: b[1][0] - b[0][0], h: b[1][1] - b[0][1], c };
        }).filter((d) => d.c && d.w > 26 && d.h > 16 && CALLOUT.indexOf(d.f.properties.name) < 0);
        const texts = gLabel.selectAll("text").data(labels).join("text")
          .attr("x", (d) => d.c[0]).attr("y", (d) => d.c[1] + 3.4)
          .attr("text-anchor", "middle")
          .attr("font-family", "var(--font-sans)").attr("font-size", 9.4).attr("font-weight", 500).attr("letter-spacing", "0.03em")
          .text((d) => d.s.abbr);

        // Leader-line callouts for the states too small to hold a label.
        const featByName = {};
        feats.forEach((f) => (featByName[f.properties.name] = f));
        const LX = 890, TOP = 118, STEP = 26;
        const calls = CALLOUT.filter((n) => featByName[n]).map((n, i) => {
          const b = path.bounds(featByName[n]);
          return { s: byName[n], from: [(b[0][0] + b[1][0]) / 2, (b[0][1] + b[1][1]) / 2], y: TOP + i * STEP };
        });
        const gc = gCall.selectAll("g").data(calls).join("g")
          .style("cursor", "pointer")
          .on("click", (e, d) => cbRef.current(d.s.abbr))
          .on("mousemove", (e, d) => {
            const r = hostRef.current.getBoundingClientRect();
            setTip({ x: e.clientX - r.left, y: e.clientY - r.top, abbr: d.s.abbr });
          })
          .on("mouseleave", () => setTip(null));
        gc.append("polyline")
          .attr("points", (d) => [d.from, [LX - 42, d.y], [LX - 7, d.y]].map((p) => p[0] + "," + p[1]).join(" "))
          .attr("fill", "none")
          .attr("stroke", "#CFCFCF").attr("stroke-width", 0.7);
        gc.append("circle")
          .attr("cx", (d) => d.from[0]).attr("cy", (d) => d.from[1]).attr("r", 1.5).attr("fill", "#767676");
        const callSwatch = gc.append("rect")
          .attr("x", LX).attr("y", (d) => d.y - 4.6).attr("width", 9.2).attr("height", 9.2).attr("rx", 1.5)
          .attr("stroke-width", 0.6);
        gc.append("text")
          .attr("x", LX + 15).attr("y", (d) => d.y + 3.4)
          .attr("font-family", "var(--font-sans)").attr("font-size", 9.4).attr("font-weight", 500).attr("letter-spacing", "0.03em")
          .attr("fill", "#3A3A3A").text((d) => d.s.abbr);

        // Merged circuit outlines, drawn only in the circuit view.
        const geomByName = {};
        topo.objects.states.geometries.forEach((g) => (geomByName[g.properties.name] = g));
        const circuitShapes = Object.entries(window.PMT.CIRCUITS).map(([n, c]) => {
          const gs = c.states.map((s) => geomByName[s]).filter(Boolean);
          const merged = topojson.merge(topo, gs);
          return { n, merged, c: proj(d3.geoCentroid(merged)) };
        });
        gCircuit.selectAll("path").data(circuitShapes).join("path")
          .attr("d", (d) => path(d.merged)).attr("fill", "none")
          .attr("stroke", "#2B57B5").attr("stroke-width", 1.2).attr("stroke-linejoin", "round");

        apiRef.current = { paths, texts, gCircuit, byName, callSwatch };
        // Size the map so the whole country is visible without scrolling on load.
        const fit = () => {
          const node = svg.node();
          if (!node || !hostRef.current) return;
          if (window.innerWidth <= 760) { node.style.maxHeight = ""; node.style.width = "100%"; return; }
          const docTop = node.getBoundingClientRect().top + window.scrollY;
          const avail = Math.max(220, Math.round(window.innerHeight - docTop - 14));
          const w = Math.min(hostRef.current.clientWidth, Math.round(avail * 960 / 520));
          node.style.maxHeight = avail + "px";
          node.style.width = w + "px";
          node.style.margin = "0 auto";
        };
        fit();
        window.addEventListener("resize", fit);
        fitRef.current = fit;
        setErr(false);
        apply();
      } catch (e) { if (!dead) setErr(e.message || String(e)); }
    })();
    return () => { dead = true; if (fitRef.current) window.removeEventListener("resize", fitRef.current); };
  }, []);

  function fillFor(s) {
    if (view === "state") return PMT_FILL.status[s.status];
    if (view === "circuit") return PMT_FILL.status[window.PMT.CIRCUITS[s.circuit].status];
    if (view === "platform") return s.platforms.includes(platform) ? "#2B57B5" : "#DEDEDE";
    return PMT_FILL.trade[s.trade];
  }

  function apply() {
    const a = apiRef.current;
    if (!a) return;
    a.paths
      .attr("fill", (f) => fillFor(a.byName[f.properties.name]))
      .attr("stroke", (f) => (a.byName[f.properties.name].abbr === selected ? "#000000" : "#FFFFFF"))
      .attr("stroke-width", (f) => (a.byName[f.properties.name].abbr === selected ? 1.8 : 0.7))
      .attr("opacity", (f) => (selected && a.byName[f.properties.name].abbr !== selected ? 0.85 : 1));
    a.texts.attr("fill", (d) => { const f = fillFor(d.s); return f === "#9EBDFF" ? "#111111" : PMT_DARK.has(f) ? "#FFFFFF" : "#3A3A3A"; });
    if (a.callSwatch) a.callSwatch.attr("fill", (d) => fillFor(d.s)).attr("stroke", (d) => (fillFor(d.s) === "#DEDEDE" ? "#A8A8A8" : fillFor(d.s)));
    a.gCircuit.attr("opacity", view === "circuit" ? 1 : 0);
  }
  React.useEffect(() => { apply(); if (fitRef.current) fitRef.current(); }, [view, platform, selected]);

  const tipState = tip && window.PMT.states.find((s) => s.abbr === tip.abbr);
  const tipText = tipState && (view === "trade"
    ? window.PMT.tradeCopy[tipState.trade][0]
    : view === "platform"
      ? (tipState.platforms.includes(platform) ? platform + " named" : "Not named")
      : view === "circuit"
        ? (tipState.circuit === "DC" ? "D.C. Circuit" : tipState.circuit + ordinal(tipState.circuit) + " Circuit") + " · " + window.PMT.statusLabel[window.PMT.CIRCUITS[tipState.circuit].status]
        : window.PMT.statusLabel[tipState.status]);

  return (
    <div style={{ position: "relative" }}>
      <div ref={hostRef}></div>
      {err ? <p className="v-label" style={{ padding: "24px 0" }}>Map geometry could not be loaded. Use the state list below.</p> : null}
      {tip && tipState ? (() => {
        const w = hostRef.current ? hostRef.current.clientWidth : 780;
        const flip = tip.x > w / 2;
        return (
          <div style={{
            position: "absolute",
            left: flip ? "auto" : tip.x + 14,
            right: flip ? (w - tip.x + 14) : "auto",
            top: Math.max(4, tip.y - 6),
            pointerEvents: "none", background: "#000", color: "#fff", padding: "6px 9px", borderRadius: "2px",
            whiteSpace: "nowrap", zIndex: 4, maxWidth: "calc(100% - 20px)", overflow: "hidden", textOverflow: "ellipsis"
          }}>
            <span style={{ fontWeight: 500, fontSize: "13px" }}>{tipState.name}</span>
            <span className="v-label" style={{ color: "rgba(255,255,255,0.66)", marginLeft: "8px", fontSize: "11px" }}>{tipText}</span>
          </div>
        );
      })() : null}
    </div>
  );
}

function ordinal(n) {
  const v = parseInt(n, 10);
  if (v === 1) return "st"; if (v === 2) return "nd"; if (v === 3) return "rd"; return "th";
}

Object.assign(window, { UsMap, PMT_FILL, ordinal });
