// @wuys/map · core/nav.js
//
// High-level navigation commands. All mutate `state.nav.{tLon, tLat, tZoom}` (targets),
// then kick the animator. Drag handlers (in interactions/) write current AND target
// directly via state mutators + `animator.snap()`.

import { clampTarget, normalizeLon, NAV_ZOOM_MIN, NAV_ZOOM_MAX, NAV_LAT_MAX } from "./state.js";
import { runTween } from "./animation.js";

export function createNav({ state, events, animator, ctx, onTweenStep }) {
  /** Active duration tween, if any. */
  let activeTween = null;
  /** Set the view center (animated). Lat/lon in degrees. */
  function setCenter({ lon, lat, zoom }) {
    if (lon != null) state.nav.tLon = lon;
    if (lat != null) state.nav.tLat = lat;
    if (zoom != null) state.nav.tZoom = zoom;
    clampTarget(state);
    animator.kick();
  }

  /** Set zoom only. */
  function setZoom(zoom) { setCenter({ zoom }); }

  /** Multiply zoom by a factor (e.g. 1.5 for zoom-in). Direct, no easing —
   * incremental zoom feels best when each step is discrete. Use flyTo for
   * smooth animated zoom transitions. */
  function zoomBy(factor) {
    const next = Math.max(NAV_ZOOM_MIN, Math.min(NAV_ZOOM_MAX, state.nav.zoom * factor));
    state.nav.zoom = next;
    state.nav.tZoom = next;
    animator.snap();
    if (onTweenStep) onTweenStep();
    events.emit("nav:change", state.nav);
  }

  /** Pan by viewport pixels. `ctx.viewportToWorld(dx, dy)` must be supplied by the renderer. */
  function panBy({ dx, dy }) {
    if (!ctx || !ctx.viewportToWorld) return;
    const delta = ctx.viewportToWorld(dx, dy);
    state.nav.tLon += delta.dLon;
    state.nav.tLat = Math.max(-NAV_LAT_MAX, Math.min(NAV_LAT_MAX, state.nav.tLat + delta.dLat));
    clampTarget(state);
    animator.kick();
  }

  /**
   * Animated fly-to a point. Properly-paced with duration + easing curve.
   * @param {{ lon, lat, zoom?, duration?: number, easing?: string }} opts
   */
  function flyTo({ lon, lat, zoom, duration = 900, easing = "easeOutCubic" }) {
    if (activeTween) activeTween.stop();
    const from = { lon: state.nav.lon, lat: state.nav.lat, zoom: state.nav.zoom };
    const to = {
      lon: lon != null ? lon : state.nav.lon,
      lat: lat != null ? lat : state.nav.lat,
      zoom: zoom != null ? zoom : state.nav.zoom,
    };
    // Clamp destination
    to.zoom = Math.max(NAV_ZOOM_MIN, Math.min(NAV_ZOOM_MAX, to.zoom));
    to.lat  = Math.max(-NAV_LAT_MAX, Math.min(NAV_LAT_MAX, to.lat));
    events.emit("nav:flyto-start", { from, to, duration, easing });
    activeTween = runTween({
      from, to, duration, easing,
      onStep: (v) => {
        state.nav.lon = v.lon; state.nav.lat = v.lat; state.nav.zoom = v.zoom;
        state.nav.tLon = v.lon; state.nav.tLat = v.lat; state.nav.tZoom = v.zoom;
        if (onTweenStep) onTweenStep();
        events.emit("nav:change", state.nav);
      },
      onDone: () => {
        activeTween = null;
        events.emit("nav:flyto-end", to);
        events.emit("nav:end", state.nav);
      },
    });
  }

  /** Fit the viewport to a country's bbox. Requires `state.data.countries.get(iso)`. */
  function fitCountry(iso, { padding = 50 } = {}) {
    const cm = state.data.countries.get(iso);
    if (!cm) return;
    // Heuristic: zoom proportional to inverse of country geographic size.
    // (Replaced by precise bbox-fit logic once renderer's viewport-math is wired.)
    const span = Math.max(cm.lonRange ?? 30, cm.latRange ?? 30);
    const zoom = Math.max(1, Math.min(NAV_ZOOM_MAX, 60 / span));
    flyTo({ lon: cm.centroid?.lon ?? 0, lat: cm.centroid?.lat ?? 0, zoom });
  }

  /** Reset to world view. */
  function reset({ animated = true } = {}) {
    state.nav.tLon = 0;
    state.nav.tLat = 0;
    state.nav.tZoom = 1;
    if (animated) {
      animator.kick();
    } else {
      animator.snap();
    }
  }

  /** Return current (animated) center for inspection / serialization. */
  function getCenter() {
    return { lon: state.nav.lon, lat: state.nav.lat, zoom: state.nav.zoom };
  }

  /** Return current target (where we're heading). */
  function getTarget() {
    return { lon: state.nav.tLon, lat: state.nav.tLat, zoom: state.nav.tZoom };
  }

  return { setCenter, setZoom, zoomBy, panBy, flyTo, fitCountry, reset, getCenter, getTarget };
}
