// @wuys/map · index.js — public API entry
//
// Phase A3 + A4 + B + C-shim + E-prep complete.
//   - Core: state, events, animation (with duration tweens), nav, hash
//   - Math: equal-earth + orthographic + antimeridian + horizon + geodesy
//   - Data: topology decoder, centroids, ISO codes, lazy loader, spotlights,
//           contested regions, baked label positions
//   - Layers: countries, flags, labels, spotlights, contested, atmosphere
//   - Renderers: SVG (primary), Canvas2D (parity), WebGL2 (stub)
//                hot-swappable at runtime via map.setRenderer()
//   - Interactions: mouse, keyboard, touch (pinch + double-tap zoom)
//   - Themes: vector, antique, toon, pixel
//   - UI: overlays (theme + view + flag + search + ? help + renderer picker)
//   - Integrations: legacy.js for one-liner mount in existing pages

import { createState, clampTarget, normalizeLon } from "./core/state.js";
import { createEventBus } from "./core/events.js";
import { createAnimator, easings, shortestLonDelta } from "./core/animation.js";
import { createNav } from "./core/nav.js";
import { attachHashSync } from "./core/hash.js";

import * as equalEarth   from "./math/projections/equal-earth.js";
import * as orthographic from "./math/projections/orthographic.js";

import { decodeTopojson, ringFromArcs } from "./data/topology.js";
import { computeCountryMeta } from "./data/centroids.js";
import { loadTopology, loadFlags, loadFlagPositions } from "./data/loader.js";
import { tableFromDict } from "./data/iso-codes.js";

import { createRenderer, detectBest, listRenderers } from "./renderers/base.js";
import "./renderers/svg.js";     // registers "svg"
import "./renderers/canvas.js";  // registers "canvas"
import "./renderers/webgl.js";   // registers "webgl"  (stub — throws on init)

import createCountriesLayer from "./layers/countries.js";
import createLabelsLayer    from "./layers/labels.js";
import createFlagsLayer     from "./layers/flags.js";
import createSpotlightsLayer from "./layers/spotlights.js";
import createContestedLayer  from "./layers/contested.js";
import createAtmosphereLayer from "./layers/atmosphere.js";

import { SPOTLIGHTS } from "./data/spotlights.js";
import { CONTESTED_REGIONS } from "./data/contested.js";
import { LABEL_OFFSETS } from "./data/label-positions.js";
import { applyLabelOffsets } from "./data/labels-convert.js";

import vectorTheme  from "./themes/vector.js";
import antiqueTheme from "./themes/antique.js";
import toonTheme    from "./themes/toon.js";
import pixelTheme   from "./themes/pixel.js";

import { attachMouse }    from "./interactions/mouse.js";
import { attachKeyboard } from "./interactions/keyboard.js";
import { attachTouch }    from "./interactions/touch.js";

export const projections = { equalEarth, orthographic };
export const themes      = {
  vector: vectorTheme,
  antique: antiqueTheme,
  toon: toonTheme,
  pixel: pixelTheme,
};
export const layers      = {
  COUNTRIES: "countries", FLAGS: "flags", LABELS: "labels",
  SPOTLIGHTS: "spotlights", CONTESTED: "contested", ATMOSPHERE: "atmosphere",
};
export { attachOverlays } from "./ui/overlays.js";

/**
 * Create a fully-wired map instance.
 *
 * @param {Object} opts
 * @param {Element} opts.target            DOM element to mount into
 * @param {"flat" | "globe"} [opts.view]   initial view
 * @param {"svg" | "canvas" | "webgl" | "auto"} [opts.renderer]
 * @param {Object} [opts.theme]            theme object (default vectorTheme)
 * @param {string[]} [opts.layers]         layer IDs to enable
 * @param {Object} [opts.position]         { lon, lat, zoom }
 * @param {(iso, fallback) => string} [opts.getCountryName]
 * @returns {Promise<MapInstance>}
 */
export async function createMap(opts = {}) {
  const target = opts.target;
  if (!target) throw new Error("[wuys/map] opts.target is required");

  const themeObj = opts.theme || vectorTheme;

  // 1. Load data
  const topoData = await loadTopology(opts.data?.topology ? { url: opts.data.topology } : {});
  const flagsData = await loadFlags(opts.data?.flags ? { url: opts.data.flags } : {});
  const flagPositions = await loadFlagPositions(opts.data?.flagPositions ? { url: opts.data.flagPositions } : {});

  const numToIso = topoData.numToIso instanceof Map
    ? topoData.numToIso
    : tableFromDict(topoData.numToIso);

  const decoded = decodeTopojson(topoData.topo);
  const countryMeta = computeCountryMeta(decoded, numToIso);

  const geomByIso = new Map();
  for (const geom of decoded.geometries) {
    const iso = numToIso.get(String(parseInt(geom.id, 10)));
    if (iso) geomByIso.set(iso, geom);
  }

  // 2. State + plumbing. Sanitize position so NaN never enters state.nav.
  const initialPos = opts.position || {};
  const safeNum = (v, fallback) => (Number.isFinite(v) ? v : fallback);
  const state = createState({
    lon: safeNum(initialPos.lon, 0),
    lat: safeNum(initialPos.lat, 0),
    zoom: safeNum(initialPos.zoom, 1),
    view: opts.view ?? "flat",
    theme: (opts.theme && opts.theme.id) || "vector",
    flagsVisible:  opts.flagsVisible  !== false,
    labelsVisible: opts.labelsVisible === true,
    layers: opts.layers ?? ["countries", "flags", "labels", "spotlights", "contested", "atmosphere"],
  });

  state.data.topology = decoded;
  state.data.countries = countryMeta;
  state.data.flags = new Map(Object.entries(flagsData || {}));
  // Hand-curated flag-stamp anchors from Natural Earth's ne_10m_admin_0_label_points.
  // Each entry is an array of { lon, lat, rank, name } points, ordered by rank
  // (0 = primary). Flag layer uses these instead of polylabel anchors when present.
  state.data.flagPositions = new Map(Object.entries(flagPositions || {}));
  state.data.isoCodes.numToA2 = numToIso;
  state.data.spotlights = opts.data?.spotlights || SPOTLIGHTS;
  state.data.contestedRegions = opts.data?.contestedRegions || CONTESTED_REGIONS;
  const offsetTable = opts.data?.labelOffsets || LABEL_OFFSETS;
  state.data.labels = applyLabelOffsets(offsetTable, countryMeta);
  state.data.ready = true;
  state._topologyArcs = decoded.arcs;
  state._geomByIso = geomByIso;

  const events = createEventBus();

  // 3. Renderer proxy — stable reference for interactions + animator. When the
  // user hot-swaps renderers, only `_active` is reassigned; everything that
  // closed over `rendererProxy` keeps working transparently.
  let _activeRenderer = null;
  const rendererProxy = {
    init:            (...a) => _activeRenderer.init(...a),
    patch:           ()     => _activeRenderer && _activeRenderer.patch(),
    destroy:         ()     => _activeRenderer && _activeRenderer.destroy(),
    hitTest:         (x, y) => _activeRenderer.hitTest(x, y),
    worldToScreen:   (p)    => _activeRenderer.worldToScreen(p),
    screenToWorld:   (p)    => _activeRenderer.screenToWorld(p),
    viewportToWorld: (dx, dy) => _activeRenderer.viewportToWorld(dx, dy),
    getViewport:     ()     => _activeRenderer.getViewport(),
    applyTheme:      ()     => _activeRenderer && _activeRenderer.applyTheme && _activeRenderer.applyTheme(),
  };

  const animator = createAnimator({
    getCurrent: () => ({ lon: state.nav.lon, lat: state.nav.lat, zoom: state.nav.zoom }),
    getTarget:  () => ({ lon: state.nav.tLon, lat: state.nav.tLat, zoom: state.nav.tZoom }),
    setCurrent: (k, v) => { state.nav[k] = v; },
    adjustDelta: shortestLonDelta,
    tolerance: { lon: 0.01, lat: 0.01, zoom: 0.003 },
    onStep: () => { normalizeLon(state); rendererProxy.patch(); events.emit("nav:change", state.nav); },
    onSettle: () => { events.emit("nav:end", state.nav); },
  });

  // 4. Build initial layers
  function buildLayerSet() {
    // Z-order (SVG document order): countries are drawn first (theme fill +
    // visible stroke). The flag layer (WebGL canvas inside a foreignObject)
    // sits above country fills so flag content covers them. Contested
    // overlays / labels / spotlights / atmosphere then layer on TOP of the
    // WebGL canvas — they stay fully visible.
    const ls = [];
    if (state.layers.enabled.has("countries"))  ls.push(createCountriesLayer());
    if (state.layers.enabled.has("flags"))      ls.push(createFlagsLayer());
    if (state.layers.enabled.has("contested"))  ls.push(createContestedLayer());
    if (state.layers.enabled.has("labels"))     ls.push(createLabelsLayer({ getCountryName: opts.getCountryName }));
    if (state.layers.enabled.has("spotlights")) ls.push(createSpotlightsLayer());
    if (state.layers.enabled.has("atmosphere")) ls.push(createAtmosphereLayer());
    return ls;
  }

  // 5. Initial renderer. Try requested renderer first; on failure, fall back
  //    to SVG so the page never goes blank if WebGL is unavailable.
  let rendererId = opts.renderer && opts.renderer !== "auto" ? opts.renderer : "svg";
  async function tryRenderer(id) {
    const r = createRenderer(id, {
      state, events, theme: themeObj, layers: buildLayerSet(), getCountryName: opts.getCountryName,
    });
    await r.init(target, { theme: themeObj });
    return r;
  }
  try {
    _activeRenderer = await tryRenderer(rendererId);
    state._activeRendererId = rendererId;
  } catch (err) {
    if (rendererId !== "svg") {
      // WebGL (or whatever was requested) failed to init — fall back to SVG.
      // The page still loads with the SVG renderer; the host can show a notice.
      console.warn("[wuys/map] renderer '" + rendererId + "' failed to init:", err && err.message);
      rendererId = "svg";
      _activeRenderer = await tryRenderer(rendererId);
      state._activeRendererId = rendererId;
    } else {
      throw err;
    }
  }

  // 6. Nav + interactions — all share the rendererProxy so hot-swap is transparent.
  const ctx = { viewportToWorld: (dx, dy) => rendererProxy.viewportToWorld(dx, dy) };
  const nav = createNav({
    state, events, animator, ctx,
    onTweenStep: () => rendererProxy.patch(),
  });

  if (opts.hashSync !== false) attachHashSync({ state, events });
  attachMouse({ target, state, events, nav, renderer: rendererProxy, animator });
  attachTouch({ target, state, events, nav, renderer: rendererProxy, animator });
  attachKeyboard({
    state, events, nav, renderer: rendererProxy,
    themesAvail: themes,
    isActive: () => target.isConnected,
  });

  events.on("view:request", (mode) => api.view.set(mode));
  events.on("flags:toggle", () => api.theme.toggleFlags());

  // 7. First paint. Defer one frame so the target's CSS layout has settled —
  // without this, a renderer that reads target.getBoundingClientRect at
  // patch time can see 0×0 and render to a degenerate viewport (the user
  // would have to interact for the resize path to fire).
  if (typeof requestAnimationFrame === "function") {
    requestAnimationFrame(() => rendererProxy.patch());
  } else {
    rendererProxy.patch();
  }

  // 8. Public API
  const api = {
    get state() { return state; },
    get renderer() { return _activeRenderer; },

    nav,
    view: {
      set(mode) {
        if (mode !== "flat" && mode !== "globe") return;
        if (state.view.mode === mode) return;
        state.view.mode = mode;
        events.emit("view:change", mode);
        rendererProxy.patch();
      },
      get() { return state.view.mode; },
      toggle() { api.view.set(state.view.mode === "flat" ? "globe" : "flat"); },
    },
    theme: {
      /** Set the visual theme (vector / antique / toon / pixel). Pure styling
       *  change — does NOT touch flag visibility or label visibility. */
      set(themeObjOrId) {
        const next = typeof themeObjOrId === "string" ? themes[themeObjOrId] : themeObjOrId;
        if (!next) return;
        Object.assign(themeObj, next);
        state.theme.id = next.id;
        rendererProxy.applyTheme();
        events.emit("theme:change", next.id);
        rendererProxy.patch();
      },
      get id() { return state.theme.id; },
      /** Toggle flag-image fills on/off. Independent of theme and labels. */
      toggleFlags() {
        state.theme.flagsVisible = !state.theme.flagsVisible;
        events.emit("theme:flags", state.theme.flagsVisible);
        rendererProxy.patch();
      },
      /** Toggle country-name labels on/off. Independent of theme and flags. */
      toggleLabels() {
        state.theme.labelsVisible = !state.theme.labelsVisible;
        events.emit("theme:labels", state.theme.labelsVisible);
        rendererProxy.patch();
      },
      get flagsVisible()  { return state.theme.flagsVisible  === true; },
      get labelsVisible() { return state.theme.labelsVisible === true; },
    },

    on:  events.on,
    off: events.off,
    once: events.once,

    worldToScreen: (p) => rendererProxy.worldToScreen(p),
    screenToWorld: (p) => rendererProxy.screenToWorld(p),
    getViewport:   ()  => rendererProxy.getViewport(),

    /** Hot-swap the active renderer. Destroys the current one, mounts the new. */
    async setRenderer(id) {
      if (id === state._activeRendererId) return;
      const next = createRenderer(id, {
        state, events, theme: themeObj, layers: buildLayerSet(), getCountryName: opts.getCountryName,
      });
      try {
        await next.init(target, { theme: themeObj });
      } catch (err) {
        events.emit("renderer:error", { id, err });
        return;
      }
      const old = _activeRenderer;
      _activeRenderer = next;
      state._activeRendererId = id;
      old.destroy();
      next.patch();
      events.emit("renderer:change", id);
    },
    getRendererId() { return state._activeRendererId; },
    listRenderers,

    destroy() {
      rendererProxy.destroy();
      events.emit("destroy", null);
      events.clear();
    },

    _internal: { state, events, animator, renderer: rendererProxy, themes, math: { equalEarth, orthographic } },
  };

  events.emit("ready", null);
  return api;
}
