/* global */
// =========================================================
// Astronomy — sun & moon, computed from lat/lon + date.
// No external libs, no fake data.
// =========================================================

// Site coordinates registry — extend as more stations come online.
// Default falls back to Praha 6 Ruzyně (LKPR).
const SITE_COORDS = {
  'praha-6-ruzyne': { lat: 50.1008, lon: 14.2632, tz: 1 },
};

function getCoords(siteId) {
  return SITE_COORDS[siteId] || SITE_COORDS['praha-6-ruzyne'];
}

const DEG = Math.PI / 180;
const RAD = 180 / Math.PI;

// Day of year (1..366)
function dayOfYear(d) {
  const start = new Date(d.getFullYear(), 0, 0);
  return Math.floor((d - start) / 86400000);
}

// Equation of time + solar declination (NOAA approximation)
function solarParams(d) {
  const N = dayOfYear(d);
  const gamma = 2 * Math.PI / 365 * (N - 1 + (d.getUTCHours() - 12) / 24);
  const eqt = 229.18 * (0.000075
    + 0.001868 * Math.cos(gamma)
    - 0.032077 * Math.sin(gamma)
    - 0.014615 * Math.cos(2 * gamma)
    - 0.040849 * Math.sin(2 * gamma));
  const decl = 0.006918
    - 0.399912 * Math.cos(gamma)
    + 0.070257 * Math.sin(gamma)
    - 0.006758 * Math.cos(2 * gamma)
    + 0.000907 * Math.sin(2 * gamma)
    - 0.002697 * Math.cos(3 * gamma)
    + 0.00148  * Math.sin(3 * gamma);
  return { eqt, decl };
}

// Returns Date objects for sunrise + sunset (local time of `d`). zenithDeg:
// 90.833 = sunrise/sunset (refraction + apparent radius), 96 = civil dawn/dusk.
function sunTimes(d, lat, lon, zenithDeg = 90.833) {
  const { eqt, decl } = solarParams(d);
  const zenith = zenithDeg * DEG;
  const cosH = (Math.cos(zenith) - Math.sin(lat * DEG) * Math.sin(decl)) /
               (Math.cos(lat * DEG) * Math.cos(decl));
  if (cosH > 1)  return { sunrise: null, sunset: null, polar: 'night' };
  if (cosH < -1) return { sunrise: null, sunset: null, polar: 'day' };
  const H = Math.acos(cosH) * RAD; // hour angle in degrees
  const solarNoon = 720 - 4 * lon - eqt; // minutes UTC
  const sunriseUTC = solarNoon - 4 * H;
  const sunsetUTC  = solarNoon + 4 * H;
  // UTC midnight of the input date's calendar day. Using Date.UTC avoids the
  // timezone trap where `new Date(y,m,d).setUTCHours(0,0,0,0)` shifts to the
  // previous calendar day for positive timezone offsets — that bug made the
  // returned sunrise/sunset belong to the wrong date in summer (CEST), so
  // "next sunrise" countdowns rounded to 0 because they were already in the
  // past relative to `now`.
  const mk = (utcMin) => new Date(
    Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) + utcMin * 60000
  );
  return { sunrise: mk(sunriseUTC), sunset: mk(sunsetUTC), polar: null };
}

// Solar elevation in degrees at the instant `d` (NOAA): true solar time →
// hour angle → zenith. Negative = below the horizon (−6° ends civil twilight).
function sunAltitude(d, lat, lon) {
  const { eqt, decl } = solarParams(d);
  const utcMin = d.getUTCHours() * 60 + d.getUTCMinutes() + d.getUTCSeconds() / 60;
  const ha = ((utcMin + eqt + 4 * lon) / 4 - 180) * DEG;
  const cosZ = Math.sin(lat * DEG) * Math.sin(decl) + Math.cos(lat * DEG) * Math.cos(decl) * Math.cos(ha);
  return 90 - Math.acos(Math.max(-1, Math.min(1, cosZ))) * RAD;
}

// Moon phase 0..1, where 0=new, 0.25=first quarter, 0.5=full, 0.75=last quarter,
// at the instant `d`: mean synodic month counted from the new moon of
// 2000-01-06 18:14 UTC (within ~half a day of the true phase).
const SYNODIC = 29.530588853;
function moonPhase(d) {
  const jd = d.getTime() / 86400000 + 2440587.5;
  const p = ((jd - 2451550.26) / SYNODIC) % 1;
  return p < 0 ? p + 1 : p;
}

// Next full moon and next new moon after `d` (Date objects).
function nextMoonPhases(d) {
  const p = moonPhase(d);
  const at = (target) => new Date(d.getTime() + (((target - p) % 1 + 1) % 1) * SYNODIC * 86400000);
  return { full: at(0.5), new: at(1) };
}

function phaseName(p) {
  if (p < 0.03 || p > 0.97) return 'novoluní';
  if (p < 0.22) return 'dorůstající srpek';
  if (p < 0.28) return 'první čtvrť';
  if (p < 0.47) return 'dorůstající měsíc';
  if (p < 0.53) return 'úplněk';
  if (p < 0.72) return 'couvající měsíc';
  if (p < 0.78) return 'poslední čtvrť';
  return 'couvající srpek';
}

// Illuminated fraction 0..1 — peaks at 1 at full moon, 0 at new moon.
function illumFraction(p) {
  // sinusoidal: |cos(πp)| gives full at 0.5
  return (1 - Math.cos(2 * Math.PI * p)) / 2;
}

// --- Moon position + rise/set ------------------------------------------
// Geocentric RA/Dec/distance of the Moon via Paul Schlyter's low-precision
// series (main perturbation terms included). Accurate to a few arc-minutes —
// good for rise/set times within ~1–2 min, which is all a widget needs.
function rev(x) { return x - Math.floor(x / 360) * 360; }

// Schlyter day number: days since 2000 Jan 0.0 TDT, with UT fraction.
function dayNumber(d) {
  const Y = d.getUTCFullYear(), M = d.getUTCMonth() + 1, D = d.getUTCDate();
  const UT = d.getUTCHours() + d.getUTCMinutes() / 60 + d.getUTCSeconds() / 3600;
  const n = 367 * Y
    - Math.floor(7 * (Y + Math.floor((M + 9) / 12)) / 4)
    + Math.floor(275 * M / 9) + D - 730530;
  return n + UT / 24;
}

function moonPosition(date) {
  const d = dayNumber(date);
  const ecl = (23.4393 - 3.563e-7 * d) * DEG;

  // Moon orbital elements
  const N = rev(125.1228 - 0.0529538083 * d);
  const i = 5.1454;
  const w = rev(318.0634 + 0.1643573223 * d);
  const a = 60.2666;            // Earth radii
  const e = 0.054900;
  const M = rev(115.3654 + 13.0649929509 * d);
  // Sun elements (needed for Moon perturbations)
  const ws = 282.9404 + 4.70935e-5 * d;
  const Ms = rev(356.0470 + 0.9856002585 * d);

  // Eccentric anomaly (iterate once — eccentricity is small)
  let E = M + RAD * e * Math.sin(M * DEG) * (1 + e * Math.cos(M * DEG));
  E = E - (E - RAD * e * Math.sin(E * DEG) - M) / (1 - e * Math.cos(E * DEG));

  const x = a * (Math.cos(E * DEG) - e);
  const y = a * Math.sqrt(1 - e * e) * Math.sin(E * DEG);
  let r = Math.sqrt(x * x + y * y);
  const v = Math.atan2(y, x) * RAD;

  // Geocentric ecliptic coords
  const xe = r * (Math.cos(N * DEG) * Math.cos((v + w) * DEG)
                - Math.sin(N * DEG) * Math.sin((v + w) * DEG) * Math.cos(i * DEG));
  const ye = r * (Math.sin(N * DEG) * Math.cos((v + w) * DEG)
                + Math.cos(N * DEG) * Math.sin((v + w) * DEG) * Math.cos(i * DEG));
  const ze = r * Math.sin((v + w) * DEG) * Math.sin(i * DEG);
  let lon = Math.atan2(ye, xe) * RAD;
  let lat = Math.atan2(ze, Math.sqrt(xe * xe + ye * ye)) * RAD;

  // Main perturbations (degrees / Earth radii)
  const Lm = rev(N + w + M);     // Moon mean longitude
  const Ls = rev(ws + Ms);       // Sun mean longitude
  const Dm = rev(Lm - Ls);       // Mean elongation
  const F  = rev(Lm - N);        // Argument of latitude
  lon += -1.274 * Math.sin((M - 2 * Dm) * DEG)
       +  0.658 * Math.sin((2 * Dm) * DEG)
       -  0.186 * Math.sin((Ms) * DEG)
       -  0.059 * Math.sin((2 * M - 2 * Dm) * DEG)
       -  0.057 * Math.sin((M - 2 * Dm + Ms) * DEG)
       +  0.053 * Math.sin((M + 2 * Dm) * DEG)
       +  0.046 * Math.sin((2 * Dm - Ms) * DEG)
       +  0.041 * Math.sin((M - Ms) * DEG)
       -  0.035 * Math.sin((Dm) * DEG)
       -  0.031 * Math.sin((M + Ms) * DEG)
       -  0.015 * Math.sin((2 * F - 2 * Dm) * DEG)
       +  0.011 * Math.sin((M - 4 * Dm) * DEG);
  lat += -0.173 * Math.sin((F - 2 * Dm) * DEG)
       -  0.055 * Math.sin((M - F - 2 * Dm) * DEG)
       -  0.046 * Math.sin((M + F - 2 * Dm) * DEG)
       +  0.033 * Math.sin((F + 2 * Dm) * DEG)
       +  0.017 * Math.sin((2 * M + F) * DEG);
  r += -0.58 * Math.cos((M - 2 * Dm) * DEG)
     -  0.46 * Math.cos((2 * Dm) * DEG);

  // Ecliptic → equatorial (geocentric, of date)
  const xg = r * Math.cos(lon * DEG) * Math.cos(lat * DEG);
  const yg = r * Math.sin(lon * DEG) * Math.cos(lat * DEG);
  const zg = r * Math.sin(lat * DEG);
  const xq = xg;
  const yq = yg * Math.cos(ecl) - zg * Math.sin(ecl);
  const zq = yg * Math.sin(ecl) + zg * Math.cos(ecl);
  const ra  = rev(Math.atan2(yq, xq) * RAD);
  const dec = Math.atan2(zq, Math.sqrt(xq * xq + yq * yq)) * RAD;

  // Local sidereal time uses the Sun's mean longitude (Schlyter): GMST0 =
  // Ls + 180°, then add the elapsed UT and the observer's east longitude.
  const UT = date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600;
  return { ra, dec, r, Ls, UT };
}

// Geocentric altitude minus the Moon's rise/set target altitude (Meeus:
// h0 = 0.7275·π − 0.566°, π = horizontal parallax). Zero crossings of this
// value are moonrise (rising) / moonset (falling).
function moonAltH0(date, lat, lon) {
  const { ra, dec, r, Ls, UT } = moonPosition(date);
  const lst = rev(rev(Ls + 180) + UT * 15 + lon);   // degrees
  let ha = lst - ra;
  ha = ((ha + 180) % 360 + 360) % 360 - 180;         // → −180..180
  const sinAlt = Math.sin(lat * DEG) * Math.sin(dec * DEG)
               + Math.cos(lat * DEG) * Math.cos(dec * DEG) * Math.cos(ha * DEG);
  const alt = Math.asin(Math.max(-1, Math.min(1, sinAlt))) * RAD;
  const hp = Math.asin(1 / r) * RAD;                  // horizontal parallax
  return { alt, h0: 0.7275 * hp - 0.566 };
}
function moonAltExcess(date, lat, lon) {
  const { alt, h0 } = moonAltH0(date, lat, lon);
  return alt - h0;
}
// Moon's altitude in degrees, shifted so it crosses 0 exactly at
// moonrise / moonset (what a chart against the horizon needs).
const moonAltitude = moonAltExcess;

// Moonrise / moonset for the local calendar day of `date`. Either may be
// null — the Moon skips a rise (or set) roughly once a month because its
// day is ~24 h 50 m. Scans in 10-min steps then linearly interpolates the
// horizon crossing (sub-minute accuracy; the curve is near-linear there).
function moonTimes(date, lat, lon) {
  const dayStart = new Date(date); dayStart.setHours(0, 0, 0, 0);
  const STEP = 10; // minutes
  let prevV = null, prevT = null;
  let moonrise = null, moonset = null;
  for (let m = 0; m <= 24 * 60; m += STEP) {
    const t = new Date(dayStart.getTime() + m * 60000);
    const v = moonAltExcess(t, lat, lon);
    if (prevV !== null) {
      if (prevV < 0 && v >= 0 && !moonrise) {
        const f = prevV / (prevV - v);
        moonrise = new Date(prevT.getTime() + f * (t - prevT));
      } else if (prevV >= 0 && v < 0 && !moonset) {
        const f = prevV / (prevV - v);
        moonset = new Date(prevT.getTime() + f * (t - prevT));
      }
    }
    prevV = v; prevT = t;
  }
  return { moonrise, moonset };
}

// Daylight length in minutes, given sunrise/sunset Date pair
function dayLength(rise, set) {
  if (!rise || !set) return null;
  return Math.round((set - rise) / 60000);
}

function fmtMinutes(min) {
  if (min == null) return '—';
  const h = Math.floor(min / 60);
  const m = min % 60;
  return `${h} h ${String(m).padStart(2,'0')} min`;
}

function fmtTime(d) {
  if (!d) return '—';
  return d.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' });
}

window.Astronomy = {
  getCoords, sunTimes, sunAltitude, moonPhase, nextMoonPhases, moonTimes, moonAltitude, phaseName, illumFraction,
  dayLength, fmtMinutes, fmtTime
};
