// Grey line — the day/night terminator, and the twilight band around it where
// HF propagation briefly does things it does at no other time.
//
// The maths is the standard low-precision solar position (Meeus, chapter 25, as
// used by NOAA's solar calculator): good to well under a degree, which is far
// finer than the band being drawn. No ephemeris, no network, no dependency —
// this has to work in a field shack with no internet.
//
// Everything here is pure so it can be tested against known positions; the
// drawing lives in the map component.

const DEG = Math.PI / 180;

// julianDay for a date, including the fraction of the day.
export function julianDay(d: Date): number {
	return d.getTime() / 86400000 + 2440587.5;
}

// Sun's declination and Greenwich hour angle, both in degrees.
//
// Returned together because they are computed from the same intermediates and
// every caller needs both — splitting them into two exported functions would
// double the work at each call for the sake of a tidier signature.
export function sunPosition(date: Date): { dec: number; gha: number } {
	const jd = julianDay(date);
	const n = jd - 2451545.0; // days from J2000.0

	// Mean longitude and mean anomaly of the Sun.
	const L = (280.460 + 0.9856474 * n) % 360;
	const g = ((357.528 + 0.9856003 * n) % 360) * DEG;

	// Ecliptic longitude: the equation of centre applied to the mean longitude.
	const lambda = (L + 1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g)) * DEG;

	// Obliquity of the ecliptic, slowly decreasing.
	const eps = (23.439 - 0.0000004 * n) * DEG;

	const dec = Math.asin(Math.sin(eps) * Math.sin(lambda)) / DEG;

	// Right ascension, kept in the same quadrant as the ecliptic longitude —
	// atan2 alone lands 180° out for half the year, which mirrors the whole
	// terminator about the prime meridian. Six months of a plausible-looking
	// wrong answer is exactly the kind of bug that survives review.
	let ra = Math.atan2(Math.cos(eps) * Math.sin(lambda), Math.cos(lambda)) / DEG;
	if (ra < 0) ra += 360;

	// Greenwich mean sidereal time, in degrees.
	const gmst = (280.46061837 + 360.98564736629 * n) % 360;

	let gha = gmst - ra;
	gha = ((gha % 360) + 360) % 360;
	return { dec, gha };
}

// terminatorLatitude returns the latitude, at a given longitude, where the Sun
// sits at `altitude` degrees above (or below) the horizon.
//
// altitude 0 is the geometric terminator; -6 is civil twilight, the outer edge
// of the grey line as operators use the term.
//
// Returns null where no such latitude exists at that longitude — inside the
// polar day or polar night, where the Sun never crosses that altitude at all.
export function terminatorLatitude(lonDeg: number, dec: number, gha: number, altitude = 0): number | null {
	const ha = (gha + lonDeg) * DEG; // local hour angle
	const d = dec * DEG;
	const h = altitude * DEG;

	// From sin(alt) = sin(dec)sin(lat) + cos(dec)cos(lat)cos(ha), solved for lat:
	//   A·sin(lat) + B·cos(lat) = sin(alt),  A = sin(dec), B = cos(dec)cos(ha)
	//
	// That has TWO solutions per meridian, and picking the wrong one is not a
	// small error — it mirrors the answer about the equator. New York at 02:00
	// local in December came out as daylight because the branch chosen put the
	// terminator at +63° where it belongs at −63°: a shaded map that looks
	// entirely plausible and is exactly wrong.
	//
	// So anchor on the geometric terminator, where the answer is unambiguous and
	// classical, and take whichever branch is nearest it. At altitude 0 that
	// reproduces it exactly; for the twilight band it follows it by continuity,
	// which is the physical requirement — the twilight curve is a small offset
	// from the terminator, never on the other side of the planet.
	const A = Math.sin(d);
	const B = Math.cos(d) * Math.cos(ha);
	const R = Math.hypot(A, B);
	if (R === 0) return null;
	const s = Math.sin(h) / R;
	if (s < -1 || s > 1) return null; // the Sun never reaches this altitude here

	// The classical terminator. Division by zero is deliberate and correct here:
	// at an equinox tan(dec) → 0 and the terminator becomes a meridian, which is
	// exactly what atan(±Infinity) = ±90° expresses.
	const t = Math.tan(d);
	if (t === 0 && Math.cos(ha) === 0) return null; // degenerate: no crossing to name
	const lat0 = Math.atan(-Math.cos(ha) / t) / DEG;

	const phi = Math.atan2(B, A); // phase of the A·sin(lat) + B·cos(lat) sum
	const fold = (x: number) => {
		let v = ((x + 180) % 360 + 360) % 360 - 180;
		if (v > 90) v = 180 - v;
		if (v < -90) v = -180 - v;
		return v;
	};
	const c1 = fold((Math.asin(s) - phi) / DEG);
	const c2 = fold((Math.PI - Math.asin(s) - phi) / DEG);
	return Math.abs(c1 - lat0) <= Math.abs(c2 - lat0) ? c1 : c2;
}

// nightPolygon returns a ring covering the part of the world where the Sun is
// below `altitude`, ready for L.polygon (as [lat, lon] pairs).
//
// The ring runs along the terminator from west to east and closes along
// whichever pole is in darkness — which pole that is flips with the season, and
// getting it wrong shades the lit half of the planet.
export function nightPolygon(date: Date, altitude = 0, stepDeg = 2): [number, number][] {
	const { dec, gha } = sunPosition(date);
	const ring: [number, number][] = [];

	for (let lon = -180; lon <= 180; lon += stepDeg) {
		const lat = terminatorLatitude(lon, dec, gha, altitude);
		// Inside a polar day/night there is no crossing; clamp to the pole so the
		// ring stays closed rather than tearing open across the map.
		ring.push([lat === null ? (dec > 0 ? -90 : 90) : lat, lon]);
	}

	// Northern summer (positive declination) → the SOUTH pole is dark.
	const pole = dec > 0 ? -90 : 90;
	ring.push([pole, 180]);
	ring.push([pole, -180]);
	return ring;
}
