/**
 * geo-utils.js — pure, dependency-free geospatial helpers for the GeoTracker
 * module. No browser APIs, no framework imports — safe to reuse anywhere
 * (including Node, web workers, or other projects).
 *
 * @module geotracker/geo-utils
 */

const EARTH_RADIUS_M = 6371008.8; // mean Earth radius (metres)
const DEG2RAD = Math.PI / 180;

/**
 * Great-circle distance between two lon/lat points using the haversine
 * formula.
 *
 * @param {number} lon1
 * @param {number} lat1
 * @param {number} lon2
 * @param {number} lat2
 * @returns {number} distance in metres
 */
export function haversineMeters(lon1, lat1, lon2, lat2) {
  const dLat = (lat2 - lat1) * DEG2RAD;
  const dLon = (lon2 - lon1) * DEG2RAD;
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(lat1 * DEG2RAD) * Math.cos(lat2 * DEG2RAD) * Math.sin(dLon / 2) ** 2;
  return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(a)));
}

/**
 * Total length of a polyline expressed as an array of points.
 * @param {Array<{lon:number, lat:number}>} points
 * @returns {number} metres
 */
export function pathLengthMeters(points) {
  let total = 0;
  for (let i = 1; i < points.length; i++) {
    total += haversineMeters(points[i - 1].lon, points[i - 1].lat, points[i].lon, points[i].lat);
  }
  return total;
}

/**
 * Format a latitude or longitude for compact display.
 * @param {number} value
 * @param {number} [decimals=5] ~1.1 m precision at 5 dp
 * @returns {string}
 */
export function formatCoord(value, decimals = 5) {
  if (value == null || Number.isNaN(value)) return '—';
  return value.toFixed(decimals);
}

// ---------------------------------------------------------------------------
// UTM (Universal Transverse Mercator) — WGS84 forward projection
//
// Self-contained implementation of the standard USGS series-expansion forward
// formulas (Snyder, "Map Projections — A Working Manual"). Accurate to a few
// millimetres within a zone, which is far beyond what a consumer GPS fix
// needs. Ghana spans UTM zones 30N and 31N, but this handles the whole globe.
// No proj4 / OpenLayers dependency so it stays usable from the pure GeoTracker
// layer and from plain display code.
// ---------------------------------------------------------------------------

const UTM_A  = 6378137.0;            // WGS84 semi-major axis (metres)
const UTM_F  = 1 / 298.257223563;    // WGS84 flattening
const UTM_K0 = 0.9996;               // UTM scale factor on the central meridian
const UTM_E2 = UTM_F * (2 - UTM_F);  // first eccentricity squared
const UTM_EP2 = UTM_E2 / (1 - UTM_E2); // second eccentricity squared (e'^2)

/**
 * Convert WGS84 latitude/longitude to UTM.
 *
 * @param {number} lat  latitude in decimal degrees  (−80 … 84 is the UTM band)
 * @param {number} lon  longitude in decimal degrees (−180 … 180)
 * @returns {{zone:number, hemisphere:'N'|'S', easting:number, northing:number}|null}
 *          null when the input is missing / not finite.
 */
export function latLonToUTM(lat, lon) {
  if (lat == null || lon == null || Number.isNaN(lat) || Number.isNaN(lon)) return null;

  // Normalise longitude to [−180, 180) so the zone maths is well-defined.
  let lonNorm = ((lon + 180) % 360 + 360) % 360 - 180;

  const zone = Math.floor((lonNorm + 180) / 6) + 1;
  const lon0 = (zone - 1) * 6 - 180 + 3;   // central meridian of the zone (deg)

  const phi    = lat * DEG2RAD;
  const lambda = lonNorm * DEG2RAD;
  const lambda0 = lon0 * DEG2RAD;

  const sinPhi = Math.sin(phi);
  const cosPhi = Math.cos(phi);
  const tanPhi = Math.tan(phi);

  const N = UTM_A / Math.sqrt(1 - UTM_E2 * sinPhi * sinPhi);
  const T = tanPhi * tanPhi;
  const C = UTM_EP2 * cosPhi * cosPhi;
  const A = cosPhi * (lambda - lambda0);

  // Meridional arc length from the equator to the given latitude.
  const M = UTM_A * (
    (1 - UTM_E2 / 4 - 3 * UTM_E2 ** 2 / 64 - 5 * UTM_E2 ** 3 / 256) * phi
    - (3 * UTM_E2 / 8 + 3 * UTM_E2 ** 2 / 32 + 45 * UTM_E2 ** 3 / 1024) * Math.sin(2 * phi)
    + (15 * UTM_E2 ** 2 / 256 + 45 * UTM_E2 ** 3 / 1024) * Math.sin(4 * phi)
    - (35 * UTM_E2 ** 3 / 3072) * Math.sin(6 * phi)
  );

  let easting = UTM_K0 * N * (
    A
    + (1 - T + C) * A ** 3 / 6
    + (5 - 18 * T + T * T + 72 * C - 58 * UTM_EP2) * A ** 5 / 120
  ) + 500000; // false easting

  let northing = UTM_K0 * (
    M + N * tanPhi * (
      A * A / 2
      + (5 - T + 9 * C + 4 * C * C) * A ** 4 / 24
      + (61 - 58 * T + T * T + 600 * C - 330 * UTM_EP2) * A ** 6 / 720
    )
  );

  const hemisphere = lat >= 0 ? 'N' : 'S';
  if (hemisphere === 'S') northing += 10000000; // false northing (southern hemi)

  return {
    zone,
    hemisphere,
    easting:  Math.round(easting),
    northing: Math.round(northing),
  };
}

/**
 * Format a latitude/longitude as a compact UTM string for display, e.g.
 * `30N 706123E 619876N`. Returns the em-dash placeholder when the position
 * is unavailable.
 *
 * @param {number} lat
 * @param {number} lon
 * @returns {string}
 */
export function formatUTM(lat, lon) {
  const u = latLonToUTM(lat, lon);
  if (!u) return '—';
  return `${u.zone}${u.hemisphere} ${u.easting}E ${u.northing}N`;
}

/**
 * Format a distance in metres into a friendly string (m below 1 km, km above).
 * @param {number} meters
 * @returns {string}
 */
export function formatDistance(meters) {
  if (meters == null || Number.isNaN(meters)) return '—';
  if (meters < 1000) return `${Math.round(meters)} m`;
  return `${(meters / 1000).toFixed(2)} km`;
}

/**
 * Format a horizontal accuracy (metres) into a friendly ± string.
 * @param {number|null} meters
 * @returns {string}
 */
export function formatAccuracy(meters) {
  if (meters == null || Number.isNaN(meters)) return '—';
  return `±${Math.round(meters)} m`;
}

/**
 * Classify a horizontal accuracy into a qualitative fix-quality bucket. Useful
 * for colour-coding the UI without needing satellite count.
 * @param {number|null} meters
 * @returns {'good'|'fair'|'poor'|'none'}
 */
export function accuracyQuality(meters) {
  if (meters == null || Number.isNaN(meters)) return 'none';
  if (meters <= 10) return 'good';
  if (meters <= 30) return 'fair';
  return 'poor';
}
