// @wuys/map · math/projections/orthographic.js
//
// Orthographic projection — "view of a sphere from infinity". One hemisphere visible.
// Used for the globe view. The viewBox is FIXED at 2000×1000 — only `baseR` (the
// globe's radius inside the viewBox) scales with zoom.
//
// Forward returns { x, y, z } in math space where the visible disc is x²+y² ≤ 1.
// Multiply by baseR and translate to (CX, CY) for viewBox coords.
// `z < 0` means the point is on the BACK hemisphere — hidden.

const CX = 1000;
const CY = 500;
const BASE_R = 450;       // baseline radius at zoom 1

/**
 * Create a projection function bound to a given (lon, lat) rotation center.
 * The returned function `(lon, lat) → {x, y, z}` is in normalized math space (-1..+1 disc).
 */
export function projector(rotLambdaRad, rotPhiRad) {
  const sinPhi0 = Math.sin(rotPhiRad);
  const cosPhi0 = Math.cos(rotPhiRad);
  return function (lon, lat) {
    const lambda = lon * Math.PI / 180;
    const phi = lat * Math.PI / 180;
    const dLam = lambda - rotLambdaRad;
    const cosLam = Math.cos(dLam);
    const sinLam = Math.sin(dLam);
    const cosPhi = Math.cos(phi);
    const sinPhi = Math.sin(phi);
    return {
      x: cosPhi * sinLam,
      y: cosPhi0 * sinPhi - sinPhi0 * cosPhi * cosLam,
      z: sinPhi0 * sinPhi + cosPhi0 * cosPhi * cosLam,
    };
  };
}

/**
 * Project + viewBox map. Returns viewBox (vbx, vby) for given lon/lat at the given
 * rotation and zoom (which selects baseR). Also returns z so caller can hide back-hemisphere.
 */
export function project(lon, lat, opts) {
  const rotLambdaRad = (opts.rotLon ?? 0) * Math.PI / 180;
  const rotPhiRad = (opts.rotLat ?? 0) * Math.PI / 180;
  const zoom = opts.zoom ?? 1;
  const baseR = BASE_R * zoom;
  const p = projector(rotLambdaRad, rotPhiRad)(lon, lat);
  return {
    vbx: CX + p.x * baseR,
    vby: CY - p.y * baseR,
    z: p.z,
    visible: p.z >= 0,
  };
}

/**
 * Inverse: given a viewBox point and current rotation/zoom, recover (lon, lat).
 * If the viewBox point is outside the visible disc, clamps to the horizon.
 */
export function unproject(vbx, vby, opts) {
  const rotLambdaRad = (opts.rotLon ?? 0) * Math.PI / 180;
  const rotPhiRad = (opts.rotLat ?? 0) * Math.PI / 180;
  const zoom = opts.zoom ?? 1;
  const baseR = BASE_R * zoom;
  const xn = (vbx - CX) / baseR;
  const yn = (CY - vby) / baseR;
  const rho2 = xn * xn + yn * yn;
  if (rho2 < 1e-12) {
    return { lon: opts.rotLon ?? 0, lat: opts.rotLat ?? 0 };
  }
  let rho = Math.sqrt(rho2);
  if (rho > 1) rho = 1;
  const c = Math.asin(rho);
  const sinC = Math.sin(c);
  const cosC = Math.cos(c);
  const sinPhi0 = Math.sin(rotPhiRad);
  const cosPhi0 = Math.cos(rotPhiRad);
  const lat = Math.asin(cosC * sinPhi0 + (yn * sinC * cosPhi0) / rho);
  const lon = rotLambdaRad + Math.atan2(xn * sinC, rho * cosPhi0 * cosC - yn * sinPhi0 * sinC);
  return { lon: lon * 180 / Math.PI, lat: lat * 180 / Math.PI };
}

export const meta = {
  id: "orthographic",
  name: "Orthographic",
  type: "azimuthal",
  equalArea: false,
  viewBox: { w: 2000, h: 1000, cx: CX, cy: CY },
  baseR: BASE_R,
};
