// Sunrise / sunset for a station, in UTC.
//
// Built on greyline.ts's sunPosition() rather than on a second implementation of
// the solar equations: the map's terminator and the entry strip's sunrise must
// never disagree, and there is a whole comment block over there about a sign
// error that produced six months of plausible-looking wrong answers. One source
// of astronomy, one place to be wrong.
//
// Everything is UTC. Amateur radio runs on UTC, the log is in UTC, and a local
// time would raise the question "local to whom — me or the DX?".
import { sunPosition } from './greyline';

const DEG = Math.PI / 180;

// Standard altitude of the Sun's centre at rise/set: half a solar diameter below
// the horizon plus atmospheric refraction.
const H0 = -0.833;

// solarAltitude returns the Sun's elevation in degrees at a place and instant.
function solarAltitude(date: Date, latDeg: number, lonDeg: number): number {
	const { dec, gha } = sunPosition(date);
	const ha = (gha + lonDeg) * DEG; // local hour angle
	const d = dec * DEG;
	const lat = latDeg * DEG;
	return Math.asin(Math.sin(lat) * Math.sin(d) + Math.cos(lat) * Math.cos(d) * Math.cos(ha)) / DEG;
}

export type SunTimes = {
	rise: string; // "HH:MM" UTC, "" when the Sun does not rise that day
	set: string;
	polarDay: boolean;   // Sun stays up all day
	polarNight: boolean; // Sun never comes up
};

// sunTimes scans the UTC day a minute at a time and reports where the Sun
// crosses the rise/set altitude.
//
// A scan, not the closed-form hour-angle formula, because the closed form needs
// special-casing for the polar cases and for days where the declination shifts
// enough to matter, and it silently returns NaN rather than saying "no sunrise
// here today". 1440 evaluations is nothing, and the failure mode is honest: no
// crossing found means no sunrise, which is a real answer above the Arctic
// circle. The crossing minute is then refined by linear interpolation, so the
// result is accurate to well under a minute despite the coarse scan.
export function sunTimes(date: Date, latDeg: number, lonDeg: number): SunTimes {
	const out: SunTimes = { rise: '', set: '', polarDay: false, polarNight: false };
	if (!isFinite(latDeg) || !isFinite(lonDeg)) return out;

	const start = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
	const minute = 60000;
	let prev = solarAltitude(new Date(start), latDeg, lonDeg);
	const anyUp = prev > H0;
	let anyDown = prev <= H0;
	let sawUp = anyUp;

	for (let i = 1; i <= 1440; i++) {
		const alt = solarAltitude(new Date(start + i * minute), latDeg, lonDeg);
		if (alt > H0) sawUp = true; else anyDown = true;
		if (prev <= H0 && alt > H0 && !out.rise) {
			out.rise = fmt(start + (i - 1 + frac(prev, alt)) * minute);
		} else if (prev > H0 && alt <= H0 && !out.set) {
			out.set = fmt(start + (i - 1 + frac(prev, alt)) * minute);
		}
		prev = alt;
	}
	if (!out.rise && !out.set) {
		out.polarDay = sawUp && !anyDown;
		out.polarNight = !sawUp;
	}
	return out;
}

// frac is where between two samples the altitude crosses H0.
function frac(a: number, b: number): number {
	const d = b - a;
	return d === 0 ? 0 : (H0 - a) / d;
}

function fmt(ms: number): string {
	const d = new Date(ms);
	const p = (n: number) => String(n).padStart(2, '0');
	return `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
}
