--- Time and astronomy calculations.
-- Ported from "Calendrical Calculations" (4th edition)
-- by Nachum Dershowitz and Edward M. Reingold.
-- Original Lisp code (CALENDRICA 4.0) is Apache 2.0 licensed.
-- @module calendrica-astro
-- @release 0.1 2026-07-19

local M = {}

local basic     = require("calendrica-basic")
local gregorian = require("calendrica-gregorian")


-- === Time unit helpers ===

--- `x` hours as a fraction of a day.
-- @tparam number x Hours.
-- @treturn number Fraction of a day.
function M.hr(x)
  return x / 24
end
local hr = M.hr

--- `x` minutes as a fraction of a day.
-- @tparam number x Minutes.
-- @treturn number Fraction of a day.
function M.mn(x)
  return x / 24 / 60
end
local mn = M.mn

--- `x` seconds as a fraction of a day.
-- @tparam number x Seconds.
-- @treturn number Fraction of a day.
function M.sec(x)
  return x / 24 / 60 / 60
end
local sec = M.sec

--- `x` meters (identity; for type clarity).
-- @tparam number x Meters.
-- @treturn number Distance in meters.
function M.mt(x)
  return x
end
local mt = M.mt

--- `x` degrees (identity; for type clarity).
-- @tparam number x Degrees.
-- @treturn number Angle in degrees.
function M.deg(x)
  return x
end

--- `x` arcminutes expressed in degrees.
-- @tparam number x Arcminutes.
-- @treturn number Degrees.
function M.mins(x)
  return x / 60
end
local mins = M.mins

--- `x` arcseconds expressed in degrees.
-- @tparam number x Arcseconds.
-- @treturn number Degrees.
function M.secs(x)
  return x / 3600
end
local secs = M.secs

--- Angle from `d` degrees, `m` arcminutes, `s` arcseconds.
-- @tparam number d Degrees.
-- @tparam number m Arcminutes.
-- @tparam number s Arcseconds.
-- @treturn number Angle in degrees.
function M.angle(d, m, s)
  return d + (m + s / 60) / 60
end
local angle = M.angle


-- === Trigonometry in degrees ===

-- Convert angle theta from radians to degrees.
local function degrees_from_radians(theta)
  return (theta / math.pi / (1/180)) % 360
end

-- Convert angle theta from degrees to radians.
local function radians_from_degrees(theta)
  return (theta % 360) * math.pi * (1/180)
end

--- Sine of `theta` (given in degrees).
-- @tparam number theta Angle in degrees.
-- @treturn number Sine value.
function M.sin_degrees(theta)
  return math.sin(radians_from_degrees(theta))
end
local sin_degrees = M.sin_degrees

--- Cosine of `theta` (given in degrees).
-- @tparam number theta Angle in degrees.
-- @treturn number Cosine value.
function M.cos_degrees(theta)
  return math.cos(radians_from_degrees(theta))
end
local cos_degrees = M.cos_degrees

--- Tangent of `theta` (given in degrees).
-- @tparam number theta Angle in degrees.
-- @treturn number Tangent value.
function M.tan_degrees(theta)
  return math.tan(radians_from_degrees(theta))
end
local tan_degrees = M.tan_degrees

--- Arctangent of `y`/`x` in degrees; returns BOGUS if both are 0.
-- @tparam number y Numerator.
-- @tparam number x Denominator.
-- @treturn number Angle in degrees, or BOGUS.
function M.arctan_degrees(y, x)
  if x == 0 and y == 0 then
    return basic.BOGUS
  end
  if x == 0 then
    return basic.sign(y) * 90
  end
  local alpha = degrees_from_radians(math.atan(y / x))
  if x >= 0 then
    return alpha % 360
  else
    return (alpha + 180) % 360
  end
end
local arctan_degrees = M.arctan_degrees

--- Arcsine of `x` in degrees.
-- @tparam number x Sine value (−1..1).
-- @treturn number Angle in degrees.
function M.arcsin_degrees(x)
  return degrees_from_radians(math.asin(x))
end
local arcsin_degrees = M.arcsin_degrees

--- Arccosine of `x` in degrees.
-- @tparam number x Cosine value (−1..1).
-- @treturn number Angle in degrees.
function M.arccos_degrees(x)
  return degrees_from_radians(math.acos(x))
end
local arccos_degrees = M.arccos_degrees


-- === Location ===

--- Construct a location from latitude, longitude, elevation, and time zone.
-- @tparam number latitude  Degrees north.
-- @tparam number longitude Degrees east.
-- @tparam number elevation Meters above sea level.
-- @tparam number zone      Hours offset from UT.
-- @treturn table {latitude, longitude, elevation, zone}
function M.location(latitude, longitude, elevation, zone)
  return {latitude, longitude, elevation, zone}
end
local location = M.location

--- Latitude (degrees north) of location `loc`.
-- @tparam table loc Location.
-- @treturn number Latitude.
function M.latitude(loc)   return loc[1] end
local latitude = M.latitude

--- Longitude (degrees east) of location `loc`.
-- @tparam table loc Location.
-- @treturn number Longitude.
function M.longitude(loc)  return loc[2] end
local longitude = M.longitude

--- Elevation (meters) of location `loc`.
-- @tparam table loc Location.
-- @treturn number Elevation.
function M.elevation(loc)  return loc[3] end
local elevation = M.elevation

--- Time zone offset (hours from UT) of location `loc`.
-- @tparam table loc Location.
-- @treturn number Zone offset.
function M.zone(loc)       return loc[4] end
local zone = M.zone

local MECCA = location(angle(21, 25, 24), angle(39, 49, 24), mt(298), hr(3))

local PADUA = location(angle(45, 24, 28), angle(11, 53, 9), mt(18), hr(1))

--- Angle (clockwise from North) to face `focus` when standing in `loc`.
-- Subject to errors near focus and its antipode.
-- @tparam table loc   Observer's location.
-- @tparam table focus Target location.
-- @treturn number Bearing in degrees.
function M.direction(loc, focus)
  local phi       = latitude(loc)
  local phi_prime = latitude(focus)
  local psi       = longitude(loc)
  local psi_prime = longitude(focus)
  local y = sin_degrees(psi_prime - psi)
  local x = cos_degrees(phi) * tan_degrees(phi_prime)
          - sin_degrees(phi) * cos_degrees(psi - psi_prime)
  if (x == 0 and y == 0) or phi_prime == 90 then
    return 0
  elseif phi_prime == -90 then
    return 180
  else
    return arctan_degrees(y, x)
  end
end


-- === Time conversions ===

--- Standard time from universal time `tee_u` at `loc`.
-- @tparam number tee_u Universal time moment.
-- @tparam table  loc   Location.
-- @treturn number Standard time moment.
function M.standard_from_universal(tee_u, loc)
  return tee_u + zone(loc)
end
local standard_from_universal = M.standard_from_universal

--- Universal time from standard time `tee_s` at `loc`.
-- @tparam number tee_s Standard time moment.
-- @tparam table  loc   Location.
-- @treturn number Universal time moment.
function M.universal_from_standard(tee_s, loc)
  return tee_s - zone(loc)
end
local universal_from_standard = M.universal_from_standard

-- Difference between UT and local mean time at longitude phi.
local function zone_from_longitude(phi)
  return phi / 360
end

--- Local time from universal time `tee_u` at `loc`.
-- @tparam number tee_u Universal time moment.
-- @tparam table  loc   Location.
-- @treturn number Local time moment.
function M.local_from_universal(tee_u, loc)
  return tee_u + zone_from_longitude(longitude(loc))
end
local local_from_universal = M.local_from_universal

--- Universal time from local time `tee_l` at `loc`.
-- @tparam number tee_l Local time moment.
-- @tparam table  loc   Location.
-- @treturn number Universal time moment.
function M.universal_from_local(tee_l, loc)
  return tee_l - zone_from_longitude(longitude(loc))
end
local universal_from_local = M.universal_from_local

local equation_of_time  -- forward declaration (used by apparent_from_local/local_from_apparent before definition)

-- Standard time from local time tee_l at loc.
local function standard_from_local(tee_l, loc)
  return standard_from_universal(universal_from_local(tee_l, loc), loc)
end

-- Local time from standard time tee_s at loc.
local function local_from_standard(tee_s, loc)
  return local_from_universal(universal_from_standard(tee_s, loc), loc)
end

-- Sundial time from local time tee_l at loc.
local function apparent_from_local(tee_l, loc)
  return tee_l + equation_of_time(universal_from_local(tee_l, loc))
end

-- Local time from sundial time tee at loc.
local function local_from_apparent(tee, loc)
  return tee - equation_of_time(universal_from_local(tee, loc))
end

--- True (apparent) time at universal time `tee_u` at `loc`.
-- @tparam number tee_u Universal time moment.
-- @tparam table  loc   Location.
-- @treturn number Apparent time moment.
function M.apparent_from_universal(tee_u, loc)
  return apparent_from_local(local_from_universal(tee_u, loc), loc)
end

-- Universal time from sundial time tee at loc.
local function universal_from_apparent(tee, loc)
  return universal_from_local(local_from_apparent(tee, loc), loc)
end

--- Universal time of true (apparent) midnight of fixed `date` at `loc`.
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Universal time moment.
function M.midnight(date, loc)
  return universal_from_apparent(date, loc)
end

--- Universal time of midday on fixed `date` at `loc`.
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Universal time moment.
function M.midday(date, loc)
  return universal_from_apparent(date + hr(12), loc)
end
local midday = M.midday


-- === Dynamical time ===

-- Noon at start of Gregorian year 2000.
local J2000 = hr(12) + gregorian.gregorian_new_year(2000)

local julian_centuries   -- forward declaration (used by obliquity etc. before definition)

--- Ephemeris correction (dynamical − universal time) at moment `tee`.
-- @tparam number tee Moment.
-- @treturn number Correction in days.
function M.ephemeris_correction(tee)
  local year = gregorian.gregorian_year_from_fixed(math.floor(tee))
  local c = gregorian.gregorian_date_difference(
    gregorian.gregorian_date(1900, gregorian.JANUARY, 1),
    gregorian.gregorian_date(year, gregorian.JULY, 1)
  ) / 36525
  local y2000  = year - 2000
  local y1700  = year - 1700
  local y1600  = year - 1600
  local y1000  = (year - 1000) / 100
  local y0     = year / 100
  local y1820  = (year - 1820) / 100
  local c2051  = (1/86400) * (-20 + 32 * ((year - 1820) / 100)^2
                   + 0.5628 * (2150 - year))
  local c2006  = (1/86400) * basic.poly(y2000, {62.92, 0.32217, 0.005589})
  local c1987  = (1/86400) * basic.poly(y2000,
                   {63.86, 0.3345, -0.060374, 0.0017275,
                    0.000651814, 0.00002373599})
  local c1900  = basic.poly(c,
                   {-0.00002, 0.000297, 0.025184, -0.181133,
                    0.553040, -0.861938, 0.677066, -0.212591})
  local c1800  = basic.poly(c,
                   {-0.000009, 0.003844, 0.083563, 0.865736,
                    4.867575, 15.845535, 31.332267, 38.291999,
                    28.316289, 11.636204, 2.043794})
  local c1700  = (1/86400) * basic.poly(y1700,
                   {8.118780842, -0.005092142, 0.003336121, -0.0000266484})
  local c1600  = (1/86400) * basic.poly(y1600,
                   {120, -0.9808, -0.01532, 0.000140272128})
  local c500   = (1/86400) * basic.poly(y1000,
                   {1574.2, -556.01, 71.23472, 0.319781,
                    -0.8503463, -0.005050998, 0.0083572073})
  local c0     = (1/86400) * basic.poly(y0,
                   {10583.6, -1014.41, 33.78311, -5.952053,
                    -0.1798452, 0.022174192, 0.0090316521})
  local other  = (1/86400) * basic.poly(y1820, {-20, 0, 32})
  if     year >= 2051 and year <= 2150 then return c2051
  elseif year >= 2006 and year <= 2050 then return c2006
  elseif year >= 1987 and year <= 2005 then return c1987
  elseif year >= 1900 and year <= 1986 then return c1900
  elseif year >= 1800 and year <= 1899 then return c1800
  elseif year >= 1700 and year <= 1799 then return c1700
  elseif year >= 1600 and year <= 1699 then return c1600
  elseif year >= 500  and year <= 1599 then return c500
  elseif year >  -500 and year < 500   then return c0
  else                                       return other
  end
end
local ephemeris_correction = M.ephemeris_correction

-- Universal moment from Dynamical time tee.
local function universal_from_dynamical(tee)
  return tee - ephemeris_correction(tee)
end

-- Dynamical time at Universal moment tee_u.
local function dynamical_from_universal(tee_u)
  return tee_u + ephemeris_correction(tee_u)
end

-- Julian centuries since 2000 at moment tee.
julian_centuries = function(tee)
  return (dynamical_from_universal(tee) - J2000) / 36525
end


-- === Sidereal time ===

--- Mean sidereal time of day from moment `tee` expressed as hour angle.
-- Adapted from Meeus, "Astronomical Algorithms," 2nd edn., 1998, p. 88.
-- @tparam number tee Universal time moment.
-- @treturn number Hour angle in degrees.
function M.sidereal_from_moment(tee)
  local c = (tee - J2000) / 36525
  return basic.poly(c, {280.46061837, 36525 * 360.98564736629,
                         0.000387933, -1/38710000}) % 360
end
local sidereal_from_moment = M.sidereal_from_moment


-- === Astronomical constants ===

local MEAN_TROPICAL_YEAR = 365.242189

local MEAN_SIDEREAL_YEAR = 365.25636

local MEAN_SYNODIC_MONTH = 29.530588861

local SPRING = 0

local SUMMER = 90

local AUTUMN = 180

local WINTER = 270

local MORNING = true

local EVENING = false

local NEW = 0

local FIRST_QUARTER = 90

local FULL = 180

local LAST_QUARTER = 270


-- === Solar calculations ===

-- Obliquity of ecliptic at moment tee.
local function obliquity(tee)
  local c = julian_centuries(tee)
  return angle(23, 26, 21.448)
    + basic.poly(c, {0,
                     angle(0, 0, -46.8150),
                     angle(0, 0, -0.00059),
                     angle(0, 0,  0.001813)})
end

--- Declination of object at latitude `beta`, longitude `lambda` at moment `tee`.
-- @tparam number tee    Universal time moment.
-- @tparam number beta   Latitude of object (degrees).
-- @tparam number lambda Longitude of object (degrees).
-- @treturn number Declination in degrees.
function M.declination(tee, beta, lambda)
  local varepsilon = obliquity(tee)
  return arcsin_degrees(
    sin_degrees(beta) * cos_degrees(varepsilon)
    + cos_degrees(beta) * sin_degrees(varepsilon) * sin_degrees(lambda)
  )
end
local declination = M.declination

--- Right ascension of object at latitude `beta`, longitude `lambda` at moment `tee`.
-- @tparam number tee    Universal time moment.
-- @tparam number beta   Latitude of object (degrees).
-- @tparam number lambda Longitude of object (degrees).
-- @treturn number Right ascension in degrees.
function M.right_ascension(tee, beta, lambda)
  local varepsilon = obliquity(tee)
  return arctan_degrees(
    sin_degrees(lambda) * cos_degrees(varepsilon)
    - tan_degrees(beta) * sin_degrees(varepsilon),
    cos_degrees(lambda)
  )
end
local right_ascension = M.right_ascension

-- Longitudinal nutation at moment tee.
local function nutation(tee)
  local c     = julian_centuries(tee)
  local cap_A = basic.poly(c, {124.90, -1934.134, 0.002063})
  local cap_B = basic.poly(c, {201.11, 72001.5377, 0.00057})
  return -0.004778 * sin_degrees(cap_A)
       + -0.0003667 * sin_degrees(cap_B)
end

-- Aberration at moment tee.
local function aberration(tee)
  local c = julian_centuries(tee)
  return 0.0000974 * cos_degrees(177.63 + 35999.01848 * c)
       - 0.005575
end

--- Longitude of sun (in degrees) at moment `tee`.
-- Adapted from Bretagnon and Simon, "Planetary Programs and Tables," 1986.
-- @tparam number tee Universal time moment.
-- @treturn number Solar longitude in degrees.
function M.solar_longitude(tee)
  local c = julian_centuries(tee)
  local coefficients = {
    403406, 195207, 119433, 112392, 3891, 2819, 1721,
    660, 350, 334, 314, 268, 242, 234, 158, 132, 129, 114,
    99, 93, 86, 78, 72, 68, 64, 46, 38, 37, 32, 29, 28, 27, 27,
    25, 24, 21, 21, 20, 18, 17, 14, 13, 13, 13, 12, 10, 10, 10, 10,
  }
  local multipliers = {
    0.9287892, 35999.1376958, 35999.4089666,
    35998.7287385, 71998.20261, 71998.4403,
    36000.35726, 71997.4812, 32964.4678,
    -19.4410, 445267.1117, 45036.8840, 3.1008,
    22518.4434, -19.9739, 65928.9345,
    9038.0293, 3034.7684, 33718.148, 3034.448,
    -2280.773, 29929.992, 31556.493, 149.588,
    9037.750, 107997.405, -4444.176, 151.771,
    67555.316, 31556.080, -4561.540,
    107996.706, 1221.655, 62894.167,
    31437.369, 14578.298, -31931.757,
    34777.243, 1221.999, 62894.511,
    -4442.039, 107997.909, 119.066, 16859.071,
    -4.578, 26895.292, -39.127, 12297.536, 90073.778,
  }
  local addends = {
    270.54861, 340.19128, 63.91854, 331.26220,
    317.843, 86.631, 240.052, 310.26, 247.23,
    260.87, 297.82, 343.14, 166.79, 81.53,
    3.50, 132.75, 182.95, 162.03, 29.8,
    266.4, 249.2, 157.6, 257.8, 185.1, 69.9,
    8.0, 197.1, 250.4, 65.3, 162.7, 341.5,
    291.6, 98.5, 146.7, 110.0, 5.2, 342.6,
    230.9, 256.1, 45.3, 242.9, 115.2, 151.8,
    285.3, 53.3, 126.6, 205.7, 85.9, 146.1,
  }
  local lambda = 282.7771834
    + 36000.76953744 * c
    + 0.000005729577951308232
      * basic.sigma(
          { coefficients, multipliers, addends },
          function(x, z, y)
            return x * sin_degrees(y + z * c)
          end
        )
  return (lambda + aberration(tee) + nutation(tee)) % 360
end
local solar_longitude = M.solar_longitude

--- Moment UT of first time at or after `tee` when solar longitude is `lambda` degrees.
-- @tparam number lambda Target solar longitude (degrees).
-- @tparam number tee    Start moment (UT).
-- @treturn number Universal time moment.
function M.solar_longitude_after(lambda, tee)
  local rate = MEAN_TROPICAL_YEAR / 360
  local tau  = tee + rate * ((lambda - solar_longitude(tee)) % 360)
  local a    = math.max(tee, tau - 5)
  local b    = tau + 5
  return basic.invert_angular(
    solar_longitude, lambda, basic.interval_closed(a, b)
  )
end
local solar_longitude_after = M.solar_longitude_after

--- Moment UT of `season` in Gregorian year `g_year`.
-- @tparam number season Solar longitude of season (e.g. SPRING=0).
-- @tparam number g_year Gregorian year.
-- @treturn number Universal time moment.
function M.season_in_gregorian(season, g_year)
  return solar_longitude_after(season, gregorian.gregorian_new_year(g_year))
end

--- Precession at moment `tee` using 0,0 as J2000 coordinates.
-- Adapted from Meeus, "Astronomical Algorithms," 2nd edn., 1998, pp. 136-137.
-- @tparam number tee Universal time moment.
-- @treturn number Precession angle in degrees.
function M.precession(tee)
  local c     = julian_centuries(tee)
  local eta   = basic.poly(c, {0, secs(47.0029), secs(-0.03302),
                                   secs(0.000060)}) % 360
  local cap_P = basic.poly(c, {174.876384, secs(-869.8089),
                                   secs(0.03536)}) % 360
  local p     = basic.poly(c, {0, secs(5029.0966), secs(1.11113),
                                   secs(0.000006)}) % 360
  local cap_A = cos_degrees(eta) * sin_degrees(cap_P)
  local cap_B = cos_degrees(cap_P)
  local arg   = arctan_degrees(cap_A, cap_B)
  return (p + cap_P - arg) % 360
end
local precession = M.precession

--- Sidereal solar longitude at moment `tee`.
-- `sidereal_start` is the precession value at the Hindu reference epoch.
-- @tparam number tee            Universal time moment.
-- @tparam number sidereal_start Precession offset (degrees).
-- @treturn number Sidereal solar longitude in degrees.
function M.sidereal_solar_longitude(tee, sidereal_start)
  return (solar_longitude(tee) - precession(tee) + sidereal_start) % 360
end

--- Approximate moment at or before `tee` when solar longitude last exceeded `lambda`.
-- @tparam number lambda Solar longitude (degrees).
-- @tparam number tee    Upper bound moment (UT).
-- @treturn number Universal time moment.
function M.estimate_prior_solar_longitude(lambda, tee)
  local rate    = MEAN_TROPICAL_YEAR / 360
  local tau     = tee - rate * ((solar_longitude(tee) - lambda) % 360)
  local cap_delta = basic.mod3(solar_longitude(tau) - lambda, -180, 180)
  return math.min(tee, tau - rate * cap_delta)
end


-- === Equation of time ===

--- Equation of time (solar noon offset) at moment `tee`.
-- @tparam number tee Moment.
-- @treturn number Fraction of a day.
function M.equation_of_time(tee)
  local c           = julian_centuries(tee)
  local lambda      = basic.poly(c, {280.46645, 36000.76983, 0.0003032})
  local anomaly     = basic.poly(c, {357.52910, 35999.05030,
                                     -0.0001559, -0.00000048})
  local eccentricity = basic.poly(c, {0.016708617, -0.000042037,
                                      -0.0000001236})
  local varepsilon  = obliquity(tee)
  local y           = tan_degrees(varepsilon / 2)^2
  local equation    = (1 / (2 * math.pi))
    * (  y * sin_degrees(2 * lambda)
       - 2 * eccentricity * sin_degrees(anomaly)
       + 4 * eccentricity * y * sin_degrees(anomaly)
         * cos_degrees(2 * lambda)
       - 0.5 * y * y * sin_degrees(4 * lambda)
       - 1.25 * eccentricity * eccentricity * sin_degrees(2 * anomaly))
  return basic.sign(equation) * math.min(math.abs(equation), hr(12))
end
equation_of_time = M.equation_of_time


-- === Sunrise / sunset / dawn / dusk ===

-- Sine of angle between sun position at tee and depression alpha at loc.
local function sine_offset(tee, loc, alpha)
  local phi      = latitude(loc)
  local tee_prime = universal_from_local(tee, loc)
  local delta    = declination(tee_prime, 0, solar_longitude(tee_prime))
  return tan_degrees(phi) * tan_degrees(delta)
       + sin_degrees(alpha) / (cos_degrees(delta) * cos_degrees(phi))
end

-- Local time near tee when sun's depression is alpha at loc; bogus if none.
local function approx_moment_of_depression(tee, loc, alpha, early)
  local try  = sine_offset(tee, loc, alpha)
  local date = basic.fixed_from_moment(tee)
  local alt
  if alpha >= 0 then
    alt = early and date or date + 1
  else
    alt = date + hr(12)
  end
  local value = math.abs(try) > 1 and sine_offset(alt, loc, alpha) or try
  if math.abs(value) <= 1 then
    local offset = basic.mod3(
      arcsin_degrees(value) / 360,
      hr(-12), hr(12)
    )
    return local_from_apparent(
      date + (early and (hr(6) - offset) or (hr(18) + offset)),
      loc
    )
  else
    return basic.BOGUS
  end
end

-- Local time near approx when sun's depression is alpha at loc; bogus if none.
local function moment_of_depression(approx, loc, alpha, early)
  local tee = approx_moment_of_depression(approx, loc, alpha, early)
  if tee == basic.BOGUS then
    return basic.BOGUS
  elseif math.abs(approx - tee) < sec(30) then
    return tee
  else
    return moment_of_depression(tee, loc, alpha, early)
  end
end

--- Standard time of dawn on fixed `date` at `loc` with depression angle `alpha`.
-- Returns BOGUS if there is no dawn.
-- @tparam number date  Fixed date.
-- @tparam table  loc   Location.
-- @tparam number alpha Depression angle (degrees).
-- @treturn number Standard time, or BOGUS.
function M.dawn(date, loc, alpha)
  local result = moment_of_depression(date + hr(6), loc, alpha, MORNING)
  if result == basic.BOGUS then
    return basic.BOGUS
  else
    return standard_from_local(result, loc)
  end
end
local dawn = M.dawn

--- Standard time of dusk on fixed `date` at `loc` with depression angle `alpha`.
-- Returns BOGUS if there is no dusk.
-- @tparam number date  Fixed date.
-- @tparam table  loc   Location.
-- @tparam number alpha Depression angle (degrees).
-- @treturn number Standard time, or BOGUS.
function M.dusk(date, loc, alpha)
  local result = moment_of_depression(date + hr(18), loc, alpha, EVENING)
  if result == basic.BOGUS then
    return basic.BOGUS
  else
    return standard_from_local(result, loc)
  end
end
local dusk = M.dusk

-- Refraction angle at moment tee at loc.
local function refraction(tee, loc) -- luacheck: ignore tee
  local h     = math.max(mt(0), elevation(loc))
  local cap_R = mt(6372000)  -- Radius of Earth.
  local dip   = arccos_degrees(cap_R / (cap_R + h))
  return mins(34) + dip + secs(19) * math.sqrt(h)
end

--- Standard time of sunrise on fixed `date` at `loc`.
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time of sunrise, or BOGUS.
function M.sunrise(date, loc)
  local alpha = refraction(date + hr(6), loc) + mins(16)
  return dawn(date, loc, alpha)
end
local sunrise = M.sunrise

--- Standard time of sunset on fixed `date` at `loc`.
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time of sunset, or BOGUS.
function M.sunset(date, loc)
  local alpha = refraction(date + hr(18), loc) + mins(16)
  return dusk(date, loc, alpha)
end
local sunset = M.sunset

-- Standard time of Jewish dusk (Vilna Gaon) on fixed date at loc.
local function jewish_dusk(date, loc)
  return dusk(date, loc, angle(4, 40, 0))
end

-- Standard time of end of Jewish sabbath (Berthold Cohn) on fixed date at loc.
local function jewish_sabbath_ends(date, loc)
  return dusk(date, loc, angle(7, 5, 0))
end

-- Length of daytime temporal hour on fixed date at loc; bogus if no sunrise/sunset.
local function daytime_temporal_hour(date, loc)
  if sunrise(date, loc) == basic.BOGUS
    or sunset(date, loc) == basic.BOGUS
  then
    return basic.BOGUS
  else
    return (sunset(date, loc) - sunrise(date, loc)) / 12
  end
end

-- Length of nighttime temporal hour on fixed date at loc; bogus if no sunrise/sunset.
local function nighttime_temporal_hour(date, loc)
  if sunrise(date + 1, loc) == basic.BOGUS
    or sunset(date, loc) == basic.BOGUS
  then
    return basic.BOGUS
  else
    return (sunrise(date + 1, loc) - sunset(date, loc)) / 12
  end
end

-- Standard time of temporal moment tee at loc; bogus if temporal hour undefined.
local function standard_from_sundial(tee, loc)
  local date = basic.fixed_from_moment(tee)
  local hour = 24 * basic.time_from_moment(tee)
  local h
  if hour >= 6 and hour <= 18 then
    h = daytime_temporal_hour(date, loc)
  elseif hour < 6 then
    h = nighttime_temporal_hour(date - 1, loc)
  else
    h = nighttime_temporal_hour(date, loc)
  end
  if h == basic.BOGUS then
    return basic.BOGUS
  elseif hour >= 6 and hour <= 18 then
    return sunrise(date, loc) + (hour - 6) * h
  elseif hour < 6 then
    return sunset(date - 1, loc) + (hour + 6) * h
  else
    return sunset(date, loc) + (hour - 18) * h
  end
end

-- Standard time of end of Jewish morning on fixed date at loc.
local function jewish_morning_end(date, loc)
  return standard_from_sundial(date + hr(10), loc)
end

-- Standard time of asr (Hanafi rule) on fixed date at loc; bogus if no asr.
local function asr(date, loc)
  local noon    = midday(date, loc)
  local phi     = latitude(loc)
  local delta   = declination(noon, 0, solar_longitude(noon))
  local altitude = arcsin_degrees(
    cos_degrees(delta) * cos_degrees(phi)
    + sin_degrees(delta) * sin_degrees(phi)
  )
  local h = basic.mod3(
    arctan_degrees(tan_degrees(altitude),
                     1 + 2 * tan_degrees(altitude)),
    -90, 90
  )
  if altitude <= 0 then
    return basic.BOGUS
  else
    return dusk(date, loc, -h)
  end
end

-- Standard time of asr (Shafi'i rule) on fixed date at loc; bogus if no asr.
local function alt_asr(date, loc) -- luacheck: ignore
  local noon    = midday(date, loc)
  local phi     = latitude(loc)
  local delta   = declination(noon, 0, solar_longitude(noon))
  local altitude = arcsin_degrees(
    cos_degrees(delta) * cos_degrees(phi)
    + sin_degrees(delta) * sin_degrees(phi)
  )
  local h = basic.mod3(
    arctan_degrees(tan_degrees(altitude),
                     1 + tan_degrees(altitude)),
    -90, 90
  )
  if altitude <= 0 then
    return basic.BOGUS
  else
    return dusk(date, loc, -h)
  end
end


-- === Italian hours ===

-- Local time of dusk in Padua, Italy on date of moment tee.
local function local_zero_hour(tee)
  local date = basic.fixed_from_moment(tee)
  return local_from_standard(
    dusk(date, PADUA, angle(0, 16, 0)) + mn(30),
    PADUA
  )
end

-- Italian time corresponding to local time tee_l.
local function italian_from_local(tee_l) -- luacheck: ignore
  local date = basic.fixed_from_moment(tee_l)
  local z0   = local_zero_hour(tee_l - 1)
  local z    = local_zero_hour(tee_l)
  if tee_l > z then
    return tee_l + (date + 1 - z)
  else
    return tee_l + (date - z0)
  end
end

-- Local time corresponding to Italian time tee.
local function local_from_italian(tee) -- luacheck: ignore
  local date = basic.fixed_from_moment(tee)
  local z    = local_zero_hour(tee - 1)
  return tee - (date - z)
end


-- === Lunar calculations ===

-- Mean longitude of moon in degrees at moment given in Julian centuries c.
local function mean_lunar_longitude(c)
  return basic.poly(c, {218.3164477, 481267.88123421,
                         -0.0015786, 1/538841, -1/65194000}) % 360
end

-- Elongation of moon in degrees at moment given in Julian centuries c.
local function lunar_elongation(c)
  return basic.poly(c, {297.8501921, 445267.1114034,
                         -0.0018819, 1/545868, -1/113065000}) % 360
end

-- Mean anomaly of sun in degrees at moment given in Julian centuries c.
local function solar_anomaly(c)
  return basic.poly(c, {357.5291092, 35999.0502909,
                         -0.0001536, 1/24490000}) % 360
end

-- Mean anomaly of moon in degrees at moment given in Julian centuries c.
local function lunar_anomaly(c)
  return basic.poly(c, {134.9633964, 477198.8675055,
                         0.0087414, 1/69699, -1/14712000}) % 360
end

-- Moon's argument of latitude in degrees at moment given in Julian centuries c.
local function moon_node(c)
  return basic.poly(c, {93.2720950, 483202.0175233,
                         -0.0036539, -1/3526000, 1/863310000}) % 360
end

-- Angular distance of the lunar node from the equinoctial point on fixed date.
local function lunar_node(date) -- luacheck: ignore
  return basic.mod3(moon_node(julian_centuries(date)), -90, 90)
end

--- Longitude of moon in degrees at moment `tee`.
-- Adapted from "Astronomical Algorithms" by Jean Meeus, 2nd edn., 1998, pp. 338-342.
-- @tparam number tee Moment (Julian day or fixed).
-- @treturn number Lunar longitude in degrees.
function M.lunar_longitude(tee)
  local c          = julian_centuries(tee)
  local cap_L_prime = mean_lunar_longitude(c)
  local cap_D      = lunar_elongation(c)
  local cap_M      = solar_anomaly(c)
  local cap_M_prime = lunar_anomaly(c)
  local cap_F      = moon_node(c)
  local cap_E      = basic.poly(c, {1, -0.002516, -0.0000074})
  local args_lunar_elongation = {
    0,2,2,0,0,0,2,2,2,2,0,1,0,2,0,0,4,0,4,2,2,1,
    1,2,2,4,2,0,2,2,1,2,0,0,2,2,2,4,0,3,2,4,0,2,
    2,2,4,0,4,1,2,0,1,3,4,2,0,1,2,
  }
  local args_solar_anomaly = {
    0,0,0,0,1,0,0,-1,0,-1,1,0,1,0,0,0,0,0,0,1,1,
    0,1,-1,0,0,0,1,0,-1,0,-2,1,2,-2,0,0,-1,0,0,1,
    -1,2,2,1,-1,0,0,-1,0,1,0,1,0,0,-1,2,1,0,
  }
  local args_lunar_anomaly = {
    1,-1,0,2,0,0,-2,-1,1,0,-1,0,1,0,1,1,-1,3,-2,
    -1,0,-1,0,1,2,0,-3,-2,-1,-2,1,0,2,0,-1,1,0,
    -1,2,-1,1,-2,-1,-1,-2,0,1,4,0,-2,0,2,1,-2,-3,
    2,1,-1,3,
  }
  local args_moon_node = {
    0,0,0,0,0,2,0,0,0,0,0,0,0,-2,2,-2,0,0,0,0,0,
    0,0,0,0,0,0,0,2,0,0,0,0,0,0,-2,2,0,2,0,0,0,0,
    0,0,-2,0,0,0,0,-2,-2,0,0,0,0,0,0,0,
  }
  local sine_coeff = {
    6288774,1274027,658314,213618,-185116,-114332,
    58793,57066,53322,45758,-40923,-34720,-30383,
    15327,-12528,10980,10675,10034,8548,-7888,
    -6766,-5163,4987,4036,3994,3861,3665,-2689,
    -2602,2390,-2348,2236,-2120,-2069,2048,-1773,
    -1595,1215,-1110,-892,-810,759,-713,-700,691,
    596,549,537,520,-487,-399,-381,351,-340,330,
    327,-323,299,294,
  }
  local correction = (1/1000000) * basic.sigma(
    {sine_coeff, args_lunar_elongation, args_solar_anomaly,
       args_lunar_anomaly, args_moon_node},
    function(v, w, x, y, z)
      return v * cap_E^math.abs(x)
           * sin_degrees(w*cap_D + x*cap_M + y*cap_M_prime + z*cap_F)
    end
  )
  local venus = (3958/1000000)
    * sin_degrees(119.75 + c * 131.849)
  local jupiter = (318/1000000)
    * sin_degrees(53.09 + c * 479264.29)
  local flat_earth = (1962/1000000)
    * sin_degrees(cap_L_prime - cap_F)
  return (cap_L_prime + correction + venus + jupiter
          + flat_earth + nutation(tee)) % 360
end
local lunar_longitude = M.lunar_longitude

-- Sidereal lunar longitude at moment tee; sidereal_start defined in Modern Hindu calendar.
local function sidereal_lunar_longitude(tee, sidereal_start) -- luacheck: ignore
  return (lunar_longitude(tee) - precession(tee) + sidereal_start) % 360
end

-- Moment of n-th new moon after/before the new moon of Jan 11, 1 CE.
local function nth_new_moon(n)
  local n0     = 24724  -- Months from RD 0 until j2000.
  local k      = n - n0
  local c      = k / 1236.85
  local approx = J2000 + basic.poly(c,
    {5.09766, MEAN_SYNODIC_MONTH * 1236.85,
     0.00015437, -0.000000150, 0.00000000073})
  local cap_E  = basic.poly(c, {1, -0.002516, -0.0000074})
  local solar_anom = basic.poly(c,
    {2.5534, 1236.85 * 29.10535670, -0.0000014, -0.00000011})
  local lunar_anom = basic.poly(c,
    {201.5643, 385.81693528 * 1236.85,
     0.0107582, 0.00001238, -0.000000058})
  local moon_arg = basic.poly(c,
    {160.7108, 390.67050284 * 1236.85,
     -0.0016118, -0.00000227, 0.000000011})
  local cap_omega = basic.poly(c,
    {124.7746, -1.56375588 * 1236.85, 0.0020672, 0.00000215})
  local e_factor    = {0,1,0,0,1,1,2,0,0,1,0,1,1,1,0,0,0,0,0,0,0,0,0,0}
  local solar_coeff = {0,1,0,0,-1,1,2,0,0,1,0,1,1,-1,2,0,3,1,0,1,-1,-1,1,0}
  local lunar_coeff = {1,0,2,0,1,1,0,1,1,2,3,0,0,2,1,2,0,1,2,1,1,1,3,4}
  local moon_coeff  = {0,0,0,2,0,0,0,-2,2,0,0,2,-2,0,0,-2,0,-2,2,2,2,-2,0,0}
  local sine_coeff  = {
    -0.40720, 0.17241, 0.01608, 0.01039,
     0.00739,-0.00514, 0.00208,-0.00111,
    -0.00057, 0.00056,-0.00042, 0.00042,
     0.00038,-0.00024,-0.00007, 0.00004,
     0.00004, 0.00003, 0.00003,-0.00003,
     0.00003,-0.00002,-0.00002, 0.00002,
  }
  local correction = -0.00017 * sin_degrees(cap_omega)
    + basic.sigma(
        { sine_coeff, e_factor, solar_coeff, lunar_coeff, moon_coeff },
        function(v, w, x, y, z)
          return v * cap_E^w
               * sin_degrees(x*solar_anom + y*lunar_anom + z*moon_arg)
        end
      )
  local add_const  = {
    251.88, 251.83, 349.42,  84.66, 141.74, 207.14, 154.84,
     34.52, 207.19, 291.34, 161.72, 239.56, 331.55,
  }
  local add_coeff  = {
    0.016321, 26.651886, 36.412478, 18.206239, 53.303771,
    2.453732,  7.306860, 27.261239,  0.121824,  1.844379,
    24.198154, 25.513099, 3.592518,
  }
  local add_factor = {
    0.000165, 0.000164, 0.000126, 0.000110, 0.000062,
    0.000060, 0.000056, 0.000047, 0.000042, 0.000040,
    0.000037, 0.000035, 0.000023,
  }
  local extra = 0.000325 * sin_degrees(
    basic.poly(c, {299.77, 132.8475848, -0.009173})
  )
  local additional = basic.sigma(
    { add_const, add_coeff, add_factor },
    function(i, j, l) return l * sin_degrees(i + j * k) end
  )
  return universal_from_dynamical(approx + correction + extra + additional)
end

--- Lunar phase as an angle in degrees at moment `tee`.
-- 0=new moon, 90=first quarter, 180=full moon, 270=last quarter.
-- @tparam number tee Moment.
-- @treturn number Lunar phase in degrees [0, 360).
function M.lunar_phase(tee)
  local phi = (lunar_longitude(tee) - solar_longitude(tee)) % 360
  local t0  = nth_new_moon(0)
  local n   = math.floor(0.5 + (tee - t0) / MEAN_SYNODIC_MONTH)
  local phi_prime = 360 * (((tee - nth_new_moon(n)) / MEAN_SYNODIC_MONTH) % 1)
  if math.abs(phi - phi_prime) > 180 then
    return phi_prime
  else
    return phi
  end
end
local lunar_phase = M.lunar_phase

--- Moment UT of last new moon before `tee`.
-- @tparam number tee Moment.
-- @treturn number Moment UT of preceding new moon.
function M.new_moon_before(tee)
  local t0  = nth_new_moon(0)
  local phi = lunar_phase(tee)
  local n   = math.floor(0.5 + (tee - t0) / MEAN_SYNODIC_MONTH
                              - phi / 360)
  return nth_new_moon(
    basic.final(n - 1, function(k) return nth_new_moon(k) < tee end)
  )
end

--- Moment UT of first new moon at or after `tee`.
-- @tparam number tee Moment.
-- @treturn number Moment UT of next new moon.
function M.new_moon_at_or_after(tee)
  local t0  = nth_new_moon(0)
  local phi = lunar_phase(tee)
  local n   = math.floor(0.5 + (tee - t0) / MEAN_SYNODIC_MONTH
                              - phi / 360)
  return nth_new_moon(
    basic.next(n, function(k) return nth_new_moon(k) >= tee end)
  )
end

--- Moment UT of the last time at or before `tee` when the lunar phase was `phi` degrees.
-- @tparam number phi Lunar phase in degrees.
-- @tparam number tee Moment.
-- @treturn number Moment UT.
function M.lunar_phase_at_or_before(phi, tee)
  local tau = tee - MEAN_SYNODIC_MONTH * (1/360)
                  * ((lunar_phase(tee) - phi) % 360)
  local a   = tau - 2
  local b   = math.min(tee, tau + 2)
  return basic.invert_angular(lunar_phase, phi, basic.interval_closed(a, b))
end

--- Moment UT of the next time at or after `tee` when the lunar phase is `phi` degrees.
-- @tparam number phi Lunar phase in degrees.
-- @tparam number tee Moment.
-- @treturn number Moment UT.
function M.lunar_phase_at_or_after(phi, tee)
  local tau = tee + MEAN_SYNODIC_MONTH * (1/360)
                  * ((phi - lunar_phase(tee)) % 360)
  local a   = math.max(tee, tau - 2)
  local b   = tau + 2
  return basic.invert_angular(lunar_phase, phi, basic.interval_closed(a, b))
end

--- Latitude of moon in degrees at moment `tee`.
-- Adapted from "Astronomical Algorithms" by Jean Meeus, 2nd edn., 1998, pp. 338-342.
-- @tparam number tee Moment.
-- @treturn number Lunar latitude in degrees.
function M.lunar_latitude(tee)
  local c           = julian_centuries(tee)
  local cap_L_prime = mean_lunar_longitude(c)
  local cap_D       = lunar_elongation(c)
  local cap_M       = solar_anomaly(c)
  local cap_M_prime = lunar_anomaly(c)
  local cap_F       = moon_node(c)
  local cap_E       = basic.poly(c, {1, -0.002516, -0.0000074})
  local args_lunar_elongation = {
    0,0,0,2,2,2,2,0,2,0,2,2,2,2,2,2,2,0,4,0,0,0,
    1,0,0,0,1,0,4,4,0,4,2,2,2,2,0,2,2,2,2,4,2,2,
    0,2,1,1,0,2,1,2,0,4,4,1,4,1,4,2,
  }
  local args_solar_anomaly = {
    0,0,0,0,0,0,0,0,0,0,-1,0,0,1,-1,-1,-1,1,0,1,
    0,1,0,1,1,1,0,0,0,0,0,0,0,0,-1,0,0,0,0,1,1,
    0,-1,-2,0,1,1,1,1,1,0,-1,1,0,-1,0,0,0,-1,-2,
  }
  local args_lunar_anomaly = {
    0,1,1,0,-1,-1,0,2,1,2,0,-2,1,0,-1,0,-1,-1,-1,
    0,0,-1,0,1,1,0,0,3,0,-1,1,-2,0,2,1,-2,3,2,-3,
    -1,0,0,1,0,1,1,0,0,-2,-1,1,-2,2,-2,-1,1,1,-1,
    0,0,
  }
  local args_moon_node = {
    1,1,-1,-1,1,-1,1,1,-1,-1,-1,-1,1,-1,1,1,-1,-1,
    -1,1,3,1,1,1,-1,-1,-1,1,-1,1,-3,1,-3,-1,-1,1,
    -1,1,-1,1,1,1,1,-1,3,-1,-1,1,-1,-1,1,-1,1,-1,
    -1,-1,-1,-1,-1,1,
  }
  local sine_coeff = {
    5128122,280602,277693,173237,55413,46271,32573,
    17198,9266,8822,8216,4324,4200,-3359,2463,2211,
    2065,-1870,1828,-1794,-1749,-1565,-1491,-1475,
    -1410,-1344,-1335,1107,1021,833,777,671,607,
    596,491,-451,439,422,421,-366,-351,331,315,
    302,-283,-229,223,223,-220,-220,-185,181,
    -177,176,166,-164,132,-119,115,107,
  }
  local beta = (1/1000000) * basic.sigma(
    { sine_coeff, args_lunar_elongation, args_solar_anomaly,
       args_lunar_anomaly, args_moon_node },
    function(v, w, x, y, z)
      return v * cap_E^math.abs(x)
           * sin_degrees(w*cap_D + x*cap_M + y*cap_M_prime + z*cap_F)
    end
  )
  local venus = (175/1000000)
    * (sin_degrees(119.75 + c * 131.849 + cap_F)
       + sin_degrees(119.75 + c * 131.849 - cap_F))
  local flat_earth = (-2235/1000000) * sin_degrees(cap_L_prime)
    + (127/1000000) * sin_degrees(cap_L_prime - cap_M_prime)
    + (-115/1000000) * sin_degrees(cap_L_prime + cap_M_prime)
  local extra = (382/1000000)
    * sin_degrees(313.45 + c * 481266.484)
  return beta + venus + flat_earth + extra
end
local lunar_latitude = M.lunar_latitude

--- Geocentric altitude of moon in degrees at `tee` at `loc`, ignoring parallax and refraction.
-- @tparam number tee Moment.
-- @tparam table  loc Location.
-- @treturn number Altitude in degrees.
function M.lunar_altitude(tee, loc)
  local phi    = latitude(loc)
  local psi    = longitude(loc)
  local lambda = lunar_longitude(tee)
  local beta   = lunar_latitude(tee)
  local alpha  = right_ascension(tee, beta, lambda)
  local delta  = declination(tee, beta, lambda)
  local theta0 = sidereal_from_moment(tee)
  local cap_H  = (theta0 + psi - alpha) % 360  -- local hour angle (note: -(-psi) = +psi... wait,
  --original uses (- psi) meaning negate psi... see below)
  -- Original: (mod (- theta0 (- psi) alpha) 360)
  -- (- psi) negates psi, so: theta0 - (-psi) - alpha = theta0 + psi - alpha
  -- But standard formula uses west-positive longitudes... keeping as original.
  local altitude = arcsin_degrees(
    sin_degrees(phi) * sin_degrees(delta)
    + cos_degrees(phi) * cos_degrees(delta) * cos_degrees(cap_H)
  )
  return basic.mod3(altitude, -180, 180)
end
local lunar_altitude = M.lunar_altitude

-- Distance to moon in meters at moment tee.
local function lunar_distance(tee)
  local c           = julian_centuries(tee)
  local cap_D       = lunar_elongation(c)
  local cap_M       = solar_anomaly(c)
  local cap_M_prime = lunar_anomaly(c)
  local cap_F       = moon_node(c)
  local cap_E       = basic.poly(c, {1, -0.002516, -0.0000074})
  local args_lunar_elongation = {
    0,2,2,0,0,0,2,2,2,2,0,1,0,2,0,0,4,0,4,2,2,1,
    1,2,2,4,2,0,2,2,1,2,0,0,2,2,2,4,0,3,2,4,0,2,
    2,2,4,0,4,1,2,0,1,3,4,2,0,1,2,2,
  }
  local args_solar_anomaly = {
    0,0,0,0,1,0,0,-1,0,-1,1,0,1,0,0,0,0,0,0,1,1,
    0,1,-1,0,0,0,1,0,-1,0,-2,1,2,-2,0,0,-1,0,0,1,
    -1,2,2,1,-1,0,0,-1,0,1,0,1,0,0,-1,2,1,0,0,
  }
  local args_lunar_anomaly = {
    1,-1,0,2,0,0,-2,-1,1,0,-1,0,1,0,1,1,-1,3,-2,
    -1,0,-1,0,1,2,0,-3,-2,-1,-2,1,0,2,0,-1,1,0,
    -1,2,-1,1,-2,-1,-1,-2,0,1,4,0,-2,0,2,1,-2,-3,
    2,1,-1,3,-1,
  }
  local args_moon_node = {
    0,0,0,0,0,2,0,0,0,0,0,0,0,-2,2,-2,0,0,0,0,0,
    0,0,0,0,0,0,0,2,0,0,0,0,0,0,-2,2,0,2,0,0,0,0,
    0,0,-2,0,0,0,0,-2,-2,0,0,0,0,0,0,0,-2,
  }
  local cosine_coeff = {
    -20905355,-3699111,-2955968,-569925,48888,-3149,
    246158,-152138,-170733,-204586,-129620,108743,
    104755,10321,0,79661,-34782,-23210,-21636,24208,
    30824,-8379,-16675,-12831,-10445,-11650,14403,
    -7003,0,10056,6322,-9884,5751,0,-4950,4130,0,
    -3958,0,3258,2616,-1897,-2117,2354,0,0,-1423,
    -1117,-1571,-1739,0,-4421,0,0,0,0,1165,0,0,
    8752,
  }
  local correction = basic.sigma(
    { cosine_coeff, args_lunar_elongation, args_solar_anomaly,
       args_lunar_anomaly, args_moon_node },
    function(v, w, x, y, z)
      return v * cap_E^math.abs(x)
           * cos_degrees(w*cap_D + x*cap_M + y*cap_M_prime + z*cap_F)
    end
  )
  return mt(385000560) + correction
end

--- Parallax of moon in degrees at `tee` at `loc`.
-- @tparam number tee Moment.
-- @tparam table  loc Location.
-- @treturn number Parallax in degrees.
function M.lunar_parallax(tee, loc)
  local geo     = lunar_altitude(tee, loc)
  local cap_delta = lunar_distance(tee)
  local alt     = mt(6378140) / cap_delta
  local arg     = alt * cos_degrees(geo)
  return arcsin_degrees(arg)
end
local lunar_parallax = M.lunar_parallax

-- Topocentric altitude of moon in degrees at tee at loc, ignoring refraction.
local function topocentric_lunar_altitude(tee, loc)
  return lunar_altitude(tee, loc) - lunar_parallax(tee, loc)
end

-- Geocentric apparent lunar diameter in degrees at moment tee.
local function lunar_diameter(tee)
  return (1792367000/9) / lunar_distance(tee)
end

-- Observed altitude of upper limb of moon at tee at loc, including refraction and elevation.
local function observed_lunar_altitude(tee, loc)
  return topocentric_lunar_altitude(tee, loc)
    + refraction(tee, loc)
    + mins(16)
end

--- Standard time of moonset on fixed `date` at `loc`.
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time of moonset, or BOGUS.
function M.moonset(date, loc)
  local tee    = universal_from_standard(date, loc)
  local waxing = lunar_phase(tee) < 180
  local alt    = observed_lunar_altitude(tee, loc)
  local lat    = latitude(loc)
  local offset = alt / (4 * (90 - math.abs(lat)))
  local approx
  if waxing then
    approx = offset > 0 and tee + offset or tee + 1 + offset
  else
    approx = tee - offset + 0.5
  end
  local set = basic.binary_search(
    approx - hr(6), approx + hr(6),
    function(x) return observed_lunar_altitude(x, loc) < 0 end,
    function(l, u) return (u - l) < mn(1) end
  )
  if set < tee + 1 then
    return math.max(standard_from_universal(set, loc), date)
  else
    return basic.BOGUS
  end
end

--- Standard time of moonrise on fixed `date` at `loc`.
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time of moonrise, or BOGUS.
function M.moonrise(date, loc)
  local tee    = universal_from_standard(date, loc)
  local waning = lunar_phase(tee) > 180
  local alt    = observed_lunar_altitude(tee, loc)
  local lat    = latitude(loc)
  local offset = alt / (4 * (90 - math.abs(lat)))
  local approx
  if waning then
    approx = offset > 0 and tee + 1 - offset or tee - offset
  else
    approx = tee + 0.5 + offset
  end
  local rise = basic.binary_search(
    approx - hr(6), approx + hr(6),
    function(x) return observed_lunar_altitude(x, loc) > 0 end,
    function(l, u) return (u - l) < mn(1) end
  )
  if rise < tee + 1 then
    return math.max(standard_from_universal(rise, loc), date)
  else
    return basic.BOGUS
  end
end



-- === Exports ===

--- Solar longitude at autumnal equinox (degrees).
M.AUTUMN                         = AUTUMN
--- Signifies evening (false).
M.EVENING                        = EVENING
--- Lunar phase at first quarter (degrees).
M.FIRST_QUARTER                  = FIRST_QUARTER
--- Lunar phase at full moon (degrees).
M.FULL                           = FULL
--- Lunar phase at last quarter (degrees).
M.LAST_QUARTER                   = LAST_QUARTER
--- Mean length of a sidereal year in days.
M.MEAN_SIDEREAL_YEAR             = MEAN_SIDEREAL_YEAR
--- Mean length of a synodic month in days.
M.MEAN_SYNODIC_MONTH             = MEAN_SYNODIC_MONTH
--- Mean length of a tropical year in days.
M.MEAN_TROPICAL_YEAR             = MEAN_TROPICAL_YEAR
--- Location of Mecca.
M.MECCA                          = MECCA
--- Signifies morning (true).
M.MORNING                        = MORNING
--- Lunar phase at new moon (degrees).
M.NEW                            = NEW
--- Location of Padua, Italy.
M.PADUA                          = PADUA
--- Solar longitude at vernal equinox (degrees).
M.SPRING                         = SPRING
--- Solar longitude at summer solstice (degrees).
M.SUMMER                         = SUMMER
--- Solar longitude at winter solstice (degrees).
M.WINTER                         = WINTER

--- Time zone offset (fraction of day) for longitude `phi` (degrees).
-- @function zone_from_longitude
-- @tparam number phi Longitude in degrees.
-- @treturn number Zone offset (fraction of day).
M.zone_from_longitude = zone_from_longitude

--- Universal time from dynamical time `tee` (subtracts ephemeris correction).
-- @function universal_from_dynamical
-- @tparam number tee Dynamical time moment.
-- @treturn number Universal time moment.
M.universal_from_dynamical = universal_from_dynamical

--- Dynamical time from universal time `tee_u` (adds ephemeris correction).
-- @function dynamical_from_universal
-- @tparam number tee_u Universal time moment.
-- @treturn number Dynamical time moment.
M.dynamical_from_universal = dynamical_from_universal

--- Length of daytime temporal hour on fixed `date` at `loc`.
-- Returns BOGUS if there is no sunrise or sunset.
-- @function daytime_temporal_hour
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Duration in days, or BOGUS.
M.daytime_temporal_hour = daytime_temporal_hour

--- Length of nighttime temporal hour on fixed `date` at `loc`.
-- Returns BOGUS if there is no sunrise or sunset.
-- @function nighttime_temporal_hour
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Duration in days, or BOGUS.
M.nighttime_temporal_hour = nighttime_temporal_hour

--- Standard time of temporal moment `tee` at `loc` (sundial time).
-- Returns BOGUS if temporal hour is undefined.
-- @function standard_from_sundial
-- @tparam number tee Moment.
-- @tparam table  loc Location.
-- @treturn number Standard time moment, or BOGUS.
M.standard_from_sundial = standard_from_sundial

--- Standard time of Jewish dusk on fixed `date` at `loc` (sun 4°40' below horizon).
-- @function jewish_dusk
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time moment.
M.jewish_dusk = jewish_dusk

--- Standard time of end of Jewish Sabbath on fixed `date` at `loc` (Berthold Cohn; sun 7°5' below horizon).
-- @function jewish_sabbath_ends
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time moment.
M.jewish_sabbath_ends = jewish_sabbath_ends

--- Standard time of end of Jewish morning on fixed `date` at `loc` (10th temporal hour).
-- @function jewish_morning_end
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time moment.
M.jewish_morning_end = jewish_morning_end

--- Standard time of Asr prayer (Hanafi rule) on fixed `date` at `loc`.
-- Returns BOGUS if no Asr occurs.
-- @function asr
-- @tparam number date Fixed date.
-- @tparam table  loc  Location.
-- @treturn number Standard time moment, or BOGUS.
M.asr = asr

--- Distance from Earth to Moon in meters at moment `tee`.
-- @function lunar_distance
-- @tparam number tee Moment.
-- @treturn number Distance in meters.
M.lunar_distance = lunar_distance

--- Geocentric apparent lunar diameter in degrees at moment `tee`.
-- @function lunar_diameter
-- @tparam number tee Moment.
-- @treturn number Diameter in degrees.
M.lunar_diameter = lunar_diameter

return M
