// Maidenhead grid locator ⇄ lat/lon, plus great-circle distance + bearing.
//
// Used to drive the "AZ SP/LP / Dist SP/LP" readouts in the entry form so
// the operator knows where to point an antenna without having to fire up an
// external tool.

const EARTH_KM = 6371.0088;
const EARTH_CIRCUMFERENCE_KM = 2 * Math.PI * EARTH_KM; // ≈ 40 030

// gridToLatLon parses a Maidenhead locator (4, 6, or 8 chars) and returns
// the center of the indicated square. Returns null on bad input.
export function gridToLatLon(grid: string): { lat: number; lon: number } | null {
  if (!grid) return null;
  const g = grid.trim().toUpperCase();
  if (g.length < 4 || g.length % 2 !== 0) return null;

  const A = 'A'.charCodeAt(0);
  const Z = 'Z'.charCodeAt(0);
  const isLetter = (c: number) => c >= A && c <= Z;
  const isDigit = (c: string) => c >= '0' && c <= '9';

  if (!isLetter(g.charCodeAt(0)) || !isLetter(g.charCodeAt(1))) return null;
  if (!isDigit(g[2]) || !isDigit(g[3])) return null;

  let lon = (g.charCodeAt(0) - A) * 20 - 180;
  let lat = (g.charCodeAt(1) - A) * 10 - 90;
  lon += parseInt(g[2], 10) * 2;
  lat += parseInt(g[3], 10);

  if (g.length >= 6) {
    if (!isLetter(g.charCodeAt(4)) || !isLetter(g.charCodeAt(5))) return null;
    lon += (g.charCodeAt(4) - A) * (2 / 24);
    lat += (g.charCodeAt(5) - A) * (1 / 24);
    // center of the sub-square
    lon += 1 / 24;
    lat += 0.5 / 24;
  } else {
    // center of the 2°×1° square
    lon += 1;
    lat += 0.5;
  }

  if (g.length >= 8) {
    if (!isDigit(g[6]) || !isDigit(g[7])) return null;
    // Extended grid (rare) — refine; using simple 10x subdivision.
    lon += parseInt(g[6], 10) * (2 / 24 / 10) - 1 / 24;
    lat += parseInt(g[7], 10) * (1 / 24 / 10) - 0.5 / 24;
  }
  return { lat, lon };
}

// latLonToGrid encodes a lat/lon to a Maidenhead locator (default 6 chars).
// Inverse of gridToLatLon. Used to derive a default grid from a cty.dat entity
// centroid when no provider grid is available (e.g. a portable/rare DX call) —
// a centre-of-entity locator is more useful than an empty field.
export function latLonToGrid(lat: number, lon: number, precision = 6): string {
  let adjLon = Math.min(359.9999, Math.max(0, lon + 180));
  let adjLat = Math.min(179.9999, Math.max(0, lat + 90));
  const A = 'A'.charCodeAt(0);
  const a = 'a'.charCodeAt(0);
  const fLon = Math.floor(adjLon / 20);
  const fLat = Math.floor(adjLat / 10);
  adjLon -= fLon * 20;
  adjLat -= fLat * 10;
  const sLon = Math.floor(adjLon / 2);
  const sLat = Math.floor(adjLat);
  let grid = String.fromCharCode(A + fLon) + String.fromCharCode(A + fLat) + sLon + sLat;
  if (precision >= 6) {
    adjLon -= sLon * 2;
    adjLat -= sLat;
    grid += String.fromCharCode(a + Math.min(23, Math.floor(adjLon * 12)))
          + String.fromCharCode(a + Math.min(23, Math.floor(adjLat * 24)));
  }
  return grid;
}

// gridSquareBounds returns the SW/NE corners of a Maidenhead square so a map
// can draw its outline. Half-extents shrink with locator precision.
export function gridSquareBounds(grid: string):
  { south: number; west: number; north: number; east: number } | null {
  const c = gridToLatLon(grid);
  if (!c) return null;
  const g = grid.trim();
  let dLon = 1, dLat = 0.5; // 4-char square: 2°×1°
  if (g.length >= 6) { dLon = 1 / 24; dLat = 0.5 / 24; }
  if (g.length >= 8) { dLon = 1 / 24 / 10; dLat = 0.5 / 24 / 10; }
  return { south: c.lat - dLat, north: c.lat + dLat, west: c.lon - dLon, east: c.lon + dLon };
}

// PathInfo describes both short and long great-circle path between two
// points. Bearing in degrees from true north (0–360). Distance in km.
export interface PathInfo {
  bearingShort: number;
  bearingLong: number;
  distanceShort: number;
  distanceLong: number;
}

// pathBetween computes great-circle bearing+distance between two
// Maidenhead grids. Returns null if either is unparseable.
export function pathBetween(fromGrid: string, toGrid: string): PathInfo | null {
  const a = gridToLatLon(fromGrid);
  const b = gridToLatLon(toGrid);
  if (!a || !b) return null;
  return pathBetweenLatLon(a, b);
}

// pathBetweenLatLon computes the great-circle path between two lat/lon points.
// Used as a fallback when a station has a known location (e.g. cty.dat entity
// coordinates for Svalbard) but no Maidenhead grid.
export function pathBetweenLatLon(
  a: { lat: number; lon: number },
  b: { lat: number; lon: number },
): PathInfo {
  const φ1 = toRad(a.lat);
  const φ2 = toRad(b.lat);
  const Δλ = toRad(b.lon - a.lon);

  // Spherical law of cosines is simpler than haversine and accurate enough
  // for ham bearings (>1 km errors don't matter at the antenna-rotor level).
  let cos = Math.sin(φ1) * Math.sin(φ2) + Math.cos(φ1) * Math.cos(φ2) * Math.cos(Δλ);
  cos = Math.max(-1, Math.min(1, cos));
  const distShort = EARTH_KM * Math.acos(cos);

  // Forward azimuth.
  const y = Math.sin(Δλ) * Math.cos(φ2);
  const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
  let bearing = toDeg(Math.atan2(y, x));
  bearing = (bearing + 360) % 360;

  return {
    bearingShort: bearing,
    bearingLong: (bearing + 180) % 360,
    distanceShort: distShort,
    distanceLong: EARTH_CIRCUMFERENCE_KM - distShort,
  };
}

// greatCirclePoints returns n+1 [lat, lon] points along the short great-circle
// path between two lat/lon points (spherical slerp). Longitudes are unwrapped
// to stay continuous (no ±180 jump) so a map polyline draws as one smooth arc.
export function greatCirclePoints(
  lat1: number, lon1: number, lat2: number, lon2: number, n = 96,
): [number, number][] {
  const φ1 = toRad(lat1), λ1 = toRad(lon1);
  const φ2 = toRad(lat2), λ2 = toRad(lon2);
  // Angular distance between the two points.
  const sinΔφ = Math.sin((φ2 - φ1) / 2);
  const sinΔλ = Math.sin((λ2 - λ1) / 2);
  const h = sinΔφ * sinΔφ + Math.cos(φ1) * Math.cos(φ2) * sinΔλ * sinΔλ;
  const d = 2 * Math.asin(Math.min(1, Math.sqrt(h)));
  const out: [number, number][] = [];
  if (d === 0) return [[lat1, lon1]];
  let prevLon = NaN;
  for (let i = 0; i <= n; i++) {
    const f = i / n;
    const A = Math.sin((1 - f) * d) / Math.sin(d);
    const B = Math.sin(f * d) / Math.sin(d);
    const x = A * Math.cos(φ1) * Math.cos(λ1) + B * Math.cos(φ2) * Math.cos(λ2);
    const y = A * Math.cos(φ1) * Math.sin(λ1) + B * Math.cos(φ2) * Math.sin(λ2);
    const z = A * Math.sin(φ1) + B * Math.sin(φ2);
    const lat = toDeg(Math.atan2(z, Math.sqrt(x * x + y * y)));
    let lon = toDeg(Math.atan2(y, x));
    // Unwrap longitude so the polyline never snaps across the whole map.
    if (!Number.isNaN(prevLon)) {
      while (lon - prevLon > 180) lon -= 360;
      while (lon - prevLon < -180) lon += 360;
    }
    prevLon = lon;
    out.push([lat, lon]);
  }
  return out;
}

// splitAtAntimeridian cuts a continuous (unwrapped) path into the pieces that
// fit on a map showing ONE world, each piece with longitudes back inside ±180.
//
// greatCirclePoints deliberately lets longitude run past ±180 so the polyline
// stays smooth. That is right for a map with repeating world copies, and wrong
// for one without: an arc from Australia to South America came out at 190°,
// 210°, 250° — drawn into the empty space off the right-hand edge, ending
// nowhere, while its own end marker sat correctly on the far left. From VK,
// where most paths cross the antimeridian, that was most of the map's arcs.
//
// Each crossing ends one piece at exactly ±180 and starts the next at the
// opposite edge, at the SAME latitude, so the line leaves one side of the map
// and re-enters the other at the height it left. Leaflet takes the result as a
// multi-polyline, so one path is still one layer.
export function splitAtAntimeridian(pts: [number, number][]): [number, number][][] {
  if (pts.length === 0) return [];
  // Which copy of the world a longitude belongs to: 0 is the map's own.
  const world = (lon: number) => Math.floor((lon + 180) / 360);
  const norm = (lon: number) => lon - 360 * world(lon);
  const out: [number, number][][] = [];
  let cur: [number, number][] = [];
  for (let i = 0; i < pts.length; i++) {
    const [lat, lon] = pts[i];
    if (i > 0) {
      const [pLat, pLon] = pts[i - 1];
      const wPrev = world(pLon), wCur = world(lon);
      if (wPrev !== wCur) {
        const east = wCur > wPrev;
        // The meridian actually crossed, in unwrapped degrees.
        const edge = 180 + 360 * Math.min(wPrev, wCur);
        const f = (edge - pLon) / (lon - pLon);
        const edgeLat = pLat + f * (lat - pLat);
        cur.push([edgeLat, east ? 180 : -180]);
        out.push(cur);
        cur = [[edgeLat, east ? -180 : 180]];
      }
    }
    cur.push([lat, norm(lon)]);
  }
  if (cur.length > 1) out.push(cur);
  else if (cur.length === 1 && out.length === 0) out.push(cur);
  return out;
}

function toRad(d: number): number { return (d * Math.PI) / 180; }
function toDeg(r: number): number { return (r * 180) / Math.PI; }

// destinationPoint returns the lat/lon reached from (lat,lon) by travelling
// distanceKm along the great circle at the given initial bearing. Used to draw
// the antenna beam lobe (a sector of bearings) on the world map.
export function destinationPoint(lat: number, lon: number, bearingDeg: number, distanceKm: number): { lat: number; lon: number } {
  const delta = distanceKm / EARTH_KM;
  const theta = toRad(bearingDeg);
  const phi1 = toRad(lat), lam1 = toRad(lon);
  const sinPhi2 = Math.sin(phi1) * Math.cos(delta) + Math.cos(phi1) * Math.sin(delta) * Math.cos(theta);
  const phi2 = Math.asin(Math.max(-1, Math.min(1, sinPhi2)));
  const y = Math.sin(theta) * Math.sin(delta) * Math.cos(phi1);
  const x = Math.cos(delta) - Math.sin(phi1) * sinPhi2;
  const lam2 = lam1 + Math.atan2(y, x);
  return { lat: toDeg(phi2), lon: ((toDeg(lam2) + 540) % 360) - 180 };
}
