// Astronomische Berechnungen fuer die Header-Anzeige: Sonnenauf-/untergang
// nach dem NOAA-Sonnenstandsalgorithmus (vereinfachte "Sunrise Equation") und
// die Mondphase nach gekuerzten Reihen aus Meeus, "Astronomical Algorithms".
// Genauigkeit reicht fuer die Anzeige und spart eine externe Bibliothek.

const DEG   = Math.PI / 180
const J1970 = 2440588      // Julianischer Tag von 1970-01-01
const J2000 = 2451545      // Julianischer Tag von 2000-01-01 12:00 UT
const DAY_MS = 86400000

// Schiefe der Ekliptik
const OBLIQUITY = 23.4397

// Standard-Hoehe der Sonnenmitte bei Auf-/Untergang: -0.833 Grad
// (34' Refraktion + 16' halber Sonnendurchmesser). Das ist die Konvention des
// Astronomical Almanac: Bezug ist ein freier Horizont auf Meereshoehe, die
// Hoehe des Beobachters geht bewusst NICHT ein.
//
// Frueher stand hier zusaetzlich eine Korrektur -2.076*sqrt(Hoehe)/60. Die gilt
// aber nur, wenn man von einer Erhebung auf einen tiefer liegenden, fernen
// Horizont blickt — am Meer. Die Station liegt auf 547 m in einem Gelaende, das
// ringsum aehnlich hoch ist; ihr realer Horizont liegt nahe 0°, nicht bei
// -0.81°. Die Korrektur hat die Zeiten um 5-6 min verschoben, weg von dem, was
// Almanach, USNO und jede Wetter-App anzeigen. Nicht wieder einbauen.
const ZENITH = -0.833

const toJulian   = date => date.getTime() / DAY_MS - 0.5 + J1970
const fromJulian = j    => new Date((j + 0.5 - J1970) * DAY_MS)

// Mittlere Entfernung Erde-Sonne in km (fuer den Phasenwinkel des Mondes)
const SUN_DISTANCE = 149598000

const sin = x => Math.sin(x * DEG)
const cos = x => Math.cos(x * DEG)
const tan = x => Math.tan(x * DEG)

// Mittlere Anomalie der Sonne, d Tage seit J2000
const sunMeanAnomaly = d => 357.5291 + 0.98560028 * d

// Ekliptikale Laenge der Sonne in Grad. Die mittlere Laenge hat eine leicht
// groessere Rate als die mittlere Anomalie — die Differenz ist die Praezession
// des Fruehlingspunkts. Wer sie weglaesst, liegt pro Jahr rund 1.7 Bogenminuten
// daneben; das verschiebt vor allem den berechneten Mondphasen-Zeitpunkt.
function sunLongitude(d) {
  const M = sunMeanAnomaly(d)
  const L0 = 280.4665 + 0.98564736 * d              // mittlere Laenge
  const C  = 1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)
  return L0 + C                                      // Mittelpunktsgleichung
}

/**
 * Berechnet Sonnenauf- und -untergang fuer einen Tag und Standort.
 *
 * @param {Date}   date  Tag, fuer den gerechnet wird (lokale Zeit)
 * @param {number} lat   Breitengrad in Grad, Nord positiv
 * @param {number} lon   Laengengrad in Grad, Ost positiv
 * @returns {{sunrise: Date|null, sunset: Date|null}}
 *          null bei Polartag/Polarnacht (Sonne geht nicht auf bzw. unter)
 */
export function getSunTimes(date, lat, lon) {
  // Bezugspunkt ist der lokale Kalendertag von `date`, nicht der UTC-Zeitpunkt.
  // Sonst faellt z.B. 00:30 Sommerzeit (= 22:30 UTC des Vortags) noch auf den
  // Vortag und wir zeigen nachts kurz die Zeiten von gestern an.
  const noon = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12)

  // Tagesnummer seit J2000 und der zugehoerige mittlere Sonnenmittag am
  // Standort (oestliche Laenge => Mittag frueher in Weltzeit).
  const n = Math.round(toJulian(noon) - J2000 + lon / 360)
  const jStar = n - lon / 360

  const M = sunMeanAnomaly(jStar)
  const lambda = sunLongitude(jStar)

  // Sonnenhoechststand (Transit)
  const jTransit = J2000 + jStar + 0.0053 * sin(M) - 0.0069 * sin(2 * lambda)

  // Deklination der Sonne
  const sinDec = sin(lambda) * sin(OBLIQUITY)
  const cosDec = Math.sqrt(1 - sinDec * sinDec)

  // Stundenwinkel bei Auf-/Untergang
  const cosOmega = (sin(ZENITH) - sin(lat) * sinDec) / (cos(lat) * cosDec)

  // |cos| > 1 => Sonne bleibt den ganzen Tag ueber bzw. unter dem Horizont
  if (cosOmega > 1)  return { sunrise: null, sunset: null }  // Polarnacht
  if (cosOmega < -1) return { sunrise: null, sunset: null }  // Polartag

  const omega = Math.acos(cosOmega) / DEG

  return {
    sunrise: fromJulian(jTransit - omega / 360),
    sunset:  fromJulian(jTransit + omega / 360),
  }
}

// --- Mondphase ---------------------------------------------------------

// Umrechnung ekliptikaler in aequatoriale Koordinaten (alles in Grad)
const rightAscension = (l, b) =>
  Math.atan2(sin(l) * cos(OBLIQUITY) - tan(b) * sin(OBLIQUITY), cos(l)) / DEG
const declination = (l, b) =>
  Math.asin(sin(b) * cos(OBLIQUITY) + cos(b) * sin(OBLIQUITY) * sin(l)) / DEG

// Position der Sonne (aequatorial + ekliptikale Laenge) fuer d Tage seit J2000
function sunPosition(d) {
  const l = sunLongitude(d)
  return { ra: rightAscension(l, 0), dec: declination(l, 0), lon: l }
}

// Position des Mondes (aequatorial) und Entfernung in km, d Tage seit J2000.
// Gekuerzte Reihe nach Meeus: mittlere Elemente plus die groessten
// periodischen Stoerungen (Evektion, Variation, jaehrliche Gleichung).
function moonPosition(d) {
  const L  = 218.3165 + 13.17639648 * d   // mittlere Laenge
  const D  = 297.8502 + 12.19074912 * d   // mittlere Elongation
  const M  = 357.5291 +  0.98560028 * d   // mittlere Anomalie der Sonne
  const Mp = 134.9634 + 13.06499295 * d   // mittlere Anomalie des Mondes
  const F  =  93.2721 + 13.22935024 * d   // Argument der Breite

  const l =
    L +
    6.289 * sin(Mp) +
    1.274 * sin(2 * D - Mp) +
    0.658 * sin(2 * D) +
    0.214 * sin(2 * Mp) -
    0.186 * sin(M) -
    0.114 * sin(2 * F)

  const b =
    5.128 * sin(F) +
    0.281 * sin(Mp + F) -
    0.278 * sin(F - Mp) -
    0.173 * sin(2 * D - F)

  const dist =
    385001 -
    20905 * cos(Mp) -
     3699 * cos(2 * D - Mp) -
     2956 * cos(2 * D) -
      570 * cos(2 * Mp)

  return { ra: rightAscension(l, b), dec: declination(l, b), dist, lon: l }
}

/**
 * Berechnet die Mondphase zu einem Zeitpunkt.
 *
 * @param {Date} date  Zeitpunkt der Berechnung
 * @returns {{fraction: number, phase: number, waxing: boolean}}
 *          fraction: beleuchteter Anteil der Mondscheibe, 0 = neu, 1 = voll
 *          phase:    Position im Zyklus, 0 = Neumond, 0.25 = erstes Viertel,
 *                    0.5 = Vollmond, 0.75 = letztes Viertel
 *          waxing:   true, solange der Mond zunimmt
 */
export function getMoonPhase(date) {
  const d = toJulian(date) - J2000
  const s = sunPosition(d)
  const m = moonPosition(d)

  // Elongation Sonne-Mond von der Erde aus gesehen
  const elongation =
    Math.acos(
      sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)
    ) / DEG

  // Phasenwinkel: Winkel Sonne-Mond-Erde
  const inc =
    Math.atan2(
      SUN_DISTANCE * sin(elongation),
      m.dist - SUN_DISTANCE * cos(elongation)
    ) / DEG

  // Position im Zyklus als ekliptikale Laengendifferenz Mond - Sonne.
  //
  // Nicht aus dem Phasenwinkel `inc` ableiten: Astronomical Almanac und USNO
  // definieren die Hauptphasen ueber genau diese Laengendifferenz (0 = Neumond,
  // 180 = Vollmond). Der Phasenwinkel laeuft davon ab, sobald der Mond weit vom
  // Knoten steht — seine bis zu 5 Grad Breite gehen dort mit ein. Bei den
  // Vierteln faellt das kaum auf, bei Neu- und Vollmond lag der berechnete
  // Zeitpunkt dadurch bis zu 3,8 h daneben.
  const phase = (((m.lon - s.lon) / 360) % 1 + 1) % 1

  return {
    fraction: (1 + cos(inc)) / 2,
    phase,
    waxing: phase < 0.5,
  }
}

// Abstand im Mondzyklus, ab dem eine Hauptphase namentlich genannt wird.
// 0.02 Zyklen entsprechen gut einem halben Tag.
const PHASE_TOLERANCE = 0.02

// Formatiert die Mondphase als deutschen Text, z.B. "Vollmond" oder
// "abnehmend 63%". Die Prozentangabe ist der beleuchtete Anteil der Scheibe.
export function formatMoonPhase({ fraction, phase, waxing }) {
  // Abstand zyklisch messen, sonst faellt die Haelfte des Neumond-Fensters
  // (phase kurz vor 1) durch das Raster.
  const near = target => {
    const diff = Math.abs(phase - target) % 1
    return Math.min(diff, 1 - diff) < PHASE_TOLERANCE
  }

  if (near(0))    return 'Neumond'
  if (near(0.25)) return 'erstes Viertel'
  if (near(0.5))  return 'Vollmond'
  if (near(0.75)) return 'letztes Viertel'

  const percent = Math.round(fraction * 100)
  return `${waxing ? 'zunehmend' : 'abnehmend'} ${percent}%`
}

// Formatiert eine Zeit als "HH:MM" in lokaler Zeit; "--:--" wenn nicht vorhanden.
export function formatTime(date) {
  if (!date) return '--:--'
  return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
}
