--- Astronomical lunar calendar conversions (Babylonian, Observational Islamic/Hebrew, Samaritan).
-- 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-astronomical-lunar
-- @release 0.1 2026-07-19

local M = {}


local basic     = require("calendrica-basic")
local gregorian = require("calendrica-gregorian")
local julian    = require("calendrica-julian")
local astro     = require("calendrica-astro")
local islamic   = require("calendrica-islamic")
local hebrew    = require("calendrica-hebrew")


-- === Locations ===

-- Location of Babylon.
local BABYLON = astro.location(32.4794, 44.4328, astro.mt(26), astro.hr(3 + 1/2))

-- Sample location for Observational Islamic calendar (Cairo, Egypt).
local ISLAMIC_LOCATION = astro.location(30.1, 31.3, astro.mt(200), astro.hr(2))

-- Location of Ankara, Turkey (used for Turkish Diyanet crescent sighting).
local ANKARA = astro.location(39.93, 32.85, astro.mt(938), astro.hr(3))

-- Location of Jerusalem.
local JERUSALEM = astro.location(31.78, 35.24, astro.mt(740), astro.hr(2))

-- Location of Acre.
local ACRE = astro.location(32.94, 35.09, astro.mt(22), astro.hr(2)) -- luacheck: ignore

-- Sample location for Observational Hebrew calendar (Haifa, Israel).
local HEBREW_LOCATION = astro.location(32.82, 35, astro.mt(0), astro.hr(2))

-- Location of Mt. Gerizim (Samaritan location).
local SAMARITAN_LOCATION = astro.location(32.1994, 35.2728, astro.mt(881), astro.hr(2))

-- Forward declaration (used by phasis_on_or_before/after before definition).
local visible_crescent


-- === Babylonian calendar ===

-- Fixed date of start of the Babylonian calendar (Seleucid era; April 3, 311 BCE Julian).
local BABYLONIAN_EPOCH = julian.fixed_from_julian(
  julian.julian_date(julian.bce(311), julian.APRIL, 3)
)

-- Babylonian date constructor.
local function babylonian_date(year, month, leap, day)
  return {year, month, leap, day}
end

-- Babylonian date accessors.
local function babylonian_year(date)  return date[1] end
local function babylonian_month(date) return date[2] end
local function babylonian_leap(date)  return date[3] end
local function babylonian_day(date)   return date[4] end

-- True if b_year is a leap year on the Babylonian calendar.
local function babylonian_leap_year(b_year)
  return (7 * b_year + 13) % 19 < 7
end

-- Time between sunset and moonset on date at location; bogus if no sunset.
local function moonlag(date, location)
  local sun  = astro.sunset(date, location)
  local moon = astro.moonset(date, location)
  if sun == basic.BOGUS then
    return basic.BOGUS
  elseif moon == basic.BOGUS then
    return astro.hr(24)
  else
    return moon - sun
  end
end

-- Moonlag criterion for visibility of crescent moon on eve of date in Babylon.
local function babylonian_criterion(date)
  local set   = astro.sunset(date - 1, BABYLON)
  local tee   = astro.universal_from_standard(set, BABYLON)
  local phase = astro.lunar_phase(tee)
  return phase > astro.NEW and phase < astro.FIRST_QUARTER
    and astro.new_moon_before(tee) <= tee - astro.hr(24)
    and moonlag(date - 1, BABYLON) > astro.mn(48)
end

-- Fixed date of start of Babylonian month on or before date (moonset lag criterion).
local function babylonian_new_month_on_or_before(date)
  local moon = basic.fixed_from_moment(
    astro.lunar_phase_at_or_before(astro.NEW, date)
  )
  local age = date - moon
  local tau
  if age <= 3 and not babylonian_criterion(date) then
    tau = moon - 30
  else
    tau = moon
  end
  return basic.next(tau, function(d) return babylonian_criterion(d) end)
end

--- Fixed date equivalent to Babylonian date `b_date`.
-- @tparam table b_date Babylonian date {year, month, leap, day}.
-- @treturn number Fixed date.
function M.fixed_from_babylonian(b_date)
  local month = babylonian_month(b_date)
  local leap  = babylonian_leap(b_date)
  local day   = babylonian_day(b_date)
  local year  = babylonian_year(b_date)
  local month1
  if leap or (year % 19 == 18 and month > 6) then
    month1 = month
  else
    month1 = month - 1
  end
  local months = basic.quotient((year - 1) * 235 + 13, 19) + month1
  local midmonth = BABYLONIAN_EPOCH
    + basic.round_half_to_even(astro.MEAN_SYNODIC_MONTH * months) + 15
  return babylonian_new_month_on_or_before(midmonth) + day - 1
end

--- Babylonian date {year, month, leap, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, leap, day}
function M.babylonian_from_fixed(date)
  local crescent = babylonian_new_month_on_or_before(date)
  local months   = basic.round_half_to_even(
    (crescent - BABYLONIAN_EPOCH) / astro.MEAN_SYNODIC_MONTH
  )
  local year     = 1 + basic.quotient(19 * months + 5, 235)
  local approx   = BABYLONIAN_EPOCH
    + basic.round_half_to_even(
        basic.quotient((year - 1) * 235 + 13, 19)
        * astro.MEAN_SYNODIC_MONTH
      )
  local new_year = babylonian_new_month_on_or_before(approx + 15)
  local month1   = 1 + basic.round_half_to_even(
    (crescent - new_year) / 29.5
  )
  local special  = year % 19 == 18
  local leap
  if special then
    leap = month1 == 7
  else
    leap = month1 == 13
  end
  local month    = (leap or (special and month1 > 6))
                   and month1 - 1 or month1
  local day      = date - crescent + 1
  return babylonian_date(year, month, leap, day)
end


-- === Crescent visibility helpers ===

-- Closest fixed date on or before date when crescent first became visible at location.
local function phasis_on_or_before(date, location)
  local moon = basic.fixed_from_moment(
    astro.lunar_phase_at_or_before(astro.NEW, date)
  )
  local age = date - moon
  local tau
  if age <= 3 and not visible_crescent(date, location) then
    tau = moon - 30
  else
    tau = moon
  end
  return basic.next(tau, function(d) return visible_crescent(d, location) end)
end

-- Closest fixed date on or after date when crescent first became visible at location.
local function phasis_on_or_after(date, location)
  local moon = basic.fixed_from_moment(
    astro.lunar_phase_at_or_before(astro.NEW, date)
  )
  local age = date - moon
  local tau
  if age >= 4 or visible_crescent(date - 1, location) then
    tau = moon + 29
  else
    tau = date
  end
  return basic.next(tau, function(d) return visible_crescent(d, location) end)
end


-- === Solar altitude (belongs conceptually in calendrica-astro but defined here) ===

-- Geocentric altitude of sun in degrees at tee at location, ignoring parallax/refraction.
local function solar_altitude(tee, location)
  local phi    = astro.latitude(location)
  local psi    = astro.longitude(location)
  local lambda = astro.solar_longitude(tee)
  local alpha  = astro.right_ascension(tee, 0, lambda)
  local delta  = astro.declination(tee, 0, lambda)
  local theta0 = astro.sidereal_from_moment(tee)
  local cap_H  = (theta0 + psi - alpha) % 360
  local altitude = astro.arcsin_degrees(
    astro.sin_degrees(phi) * astro.sin_degrees(delta)
    + astro.cos_degrees(phi) * astro.cos_degrees(delta)
      * astro.cos_degrees(cap_H)
  )
  return basic.mod3(altitude, -180, 180)
end


-- === Crescent visibility criteria ===

-- Angular separation of sun and moon at moment tee.
local function arc_of_light(tee)
  return astro.arccos_degrees(
    astro.cos_degrees(astro.lunar_latitude(tee))
    * astro.cos_degrees(astro.lunar_phase(tee))
  )
end

-- Angular difference in altitudes of sun and moon at tee at location.
local function arc_of_vision(tee, location)
  return astro.lunar_altitude(tee, location)
    - solar_altitude(tee, location)
end

-- Topocentric lunar semi-diameter at moment tee at location.
local function lunar_semi_diameter(tee, location)
  local h = astro.lunar_altitude(tee, location)
  local p = astro.lunar_parallax(tee, location)
  return 0.27245 * p * (1 + astro.sin_degrees(h) * astro.sin_degrees(p))
end

-- Best viewing time (UT) in the evening, simple version.
local function simple_best_view(date, location)
  local dark = astro.dusk(date, location, 4.5)
  local best = dark == basic.BOGUS and date + 1 or dark
  return astro.universal_from_standard(best, location)
end

-- Best viewing time (UT) in the evening, Yallop/Bruin (1977) version.
local function bruin_best_view(date, location)
  local sun  = astro.sunset(date, location)
  local moon = astro.moonset(date, location)
  local best
  if sun == basic.BOGUS or moon == basic.BOGUS then
    best = date + 1
  else
    best = (5/9) * sun + (4/9) * moon
  end
  return astro.universal_from_standard(best, location)
end

-- Shaukat criterion for likely crescent visibility on eve of date at location.
local function shaukat_criterion(date, location)
  local tee      = simple_best_view(date - 1, location)
  local phase    = astro.lunar_phase(tee)
  local h        = astro.lunar_altitude(tee, location)
  local cap_ARCL = arc_of_light(tee)
  return phase > astro.NEW and phase < astro.FIRST_QUARTER
    and cap_ARCL >= 10.6 and cap_ARCL <= 90
    and h > 4.1
end

-- Yallop criterion for possible crescent visibility on eve of date at location.
local function yallop_criterion(date, location)
  local tee      = bruin_best_view(date - 1, location)
  local phase    = astro.lunar_phase(tee)
  local cap_D    = lunar_semi_diameter(tee, location)
  local cap_ARCL = arc_of_light(tee)
  local cap_W    = cap_D * (1 - astro.cos_degrees(cap_ARCL))
  local cap_ARCV = arc_of_vision(tee, location)
  local e        = -0.14
  local q1       = basic.poly(cap_W, {11.8371, -6.3226, 0.7319, -0.1018})
  return phase > astro.NEW and phase < astro.FIRST_QUARTER
    and cap_ARCV > q1 + e
end

-- Criterion for possible crescent visibility on eve of date at location (Shaukat's rule).
visible_crescent = function(date, location)
  return shaukat_criterion(date, location)
end


-- === Observational Islamic calendar ===

--- Fixed date equivalent to Observational Islamic date `i_date`.
-- @tparam table i_date Islamic date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_observational_islamic(i_date)
  local month = basic.standard_month(i_date)
  local day   = basic.standard_day(i_date)
  local year  = basic.standard_year(i_date)
  local midmonth = islamic.ISLAMIC_EPOCH
    + math.floor(
        ((year - 1) * 12 + month - 1/2)
        * astro.MEAN_SYNODIC_MONTH
      )
  return phasis_on_or_before(midmonth, ISLAMIC_LOCATION) + day - 1
end

--- Observational Islamic date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.observational_islamic_from_fixed(date)
  local crescent       = phasis_on_or_before(date, ISLAMIC_LOCATION)
  local elapsed_months = basic.round_half_to_even(
    (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH
  )
  local year  = 1 + basic.quotient(elapsed_months, 12)
  local month = 1 + elapsed_months % 12
  local day   = 1 + date - crescent
  return islamic.islamic_date(year, month, day)
end

-- Saudi visibility criterion on eve of fixed date in Mecca.
local function saudi_criterion(date)
  local set   = astro.sunset(date - 1, astro.MECCA)
  local tee   = astro.universal_from_standard(set, astro.MECCA)
  local phase = astro.lunar_phase(tee)
  return phase > astro.NEW and phase < astro.FIRST_QUARTER
    and moonlag(date - 1, astro.MECCA) > 0
end

-- Closest fixed date on or before date when Saudi visibility criterion held.
local function saudi_new_month_on_or_before(date)
  local moon = basic.fixed_from_moment(
    astro.lunar_phase_at_or_before(astro.NEW, date)
  )
  local age = date - moon
  local tau
  if age <= 3 and not saudi_criterion(date) then
    tau = moon - 30
  else
    tau = moon
  end
  return basic.next(tau, function(d) return saudi_criterion(d) end)
end

--- Fixed date equivalent to Saudi Islamic date `s_date`.
-- @tparam table s_date Islamic date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_saudi_islamic(s_date)
  local month = basic.standard_month(s_date)
  local day   = basic.standard_day(s_date)
  local year  = basic.standard_year(s_date)
  local midmonth = islamic.ISLAMIC_EPOCH
    + math.floor(
        ((year - 1) * 12 + month - 1/2)
        * astro.MEAN_SYNODIC_MONTH
      )
  return saudi_new_month_on_or_before(midmonth) + day - 1
end

--- Saudi Islamic date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.saudi_islamic_from_fixed(date)
  local crescent       = saudi_new_month_on_or_before(date)
  local elapsed_months = basic.round_half_to_even(
    (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH
  )
  local year  = 1 + basic.quotient(elapsed_months, 12)
  local month = 1 + elapsed_months % 12
  local day   = 1 + date - crescent
  return islamic.islamic_date(year, month, day)
end


-- === Turkish Islamic calendar (Diyanet) ===
-- The Turkish Directorate of Religious Affairs (Diyanet İşleri Başkanlığı)
-- declares the start of each Islamic month based on crescent visibility as
-- seen from Turkey.  Modelled here using the Shaukat criterion at Ankara.

--- Fixed date equivalent to Turkish Islamic date `i_date`.
-- @tparam table i_date Islamic date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_turkish_islamic(i_date)
  local month = basic.standard_month(i_date)
  local day   = basic.standard_day(i_date)
  local year  = basic.standard_year(i_date)
  local midmonth = islamic.ISLAMIC_EPOCH
    + math.floor(
        ((year - 1) * 12 + month - 1/2)
        * astro.MEAN_SYNODIC_MONTH
      )
  return phasis_on_or_before(midmonth, ANKARA) + day - 1
end

--- Turkish Islamic date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.turkish_islamic_from_fixed(date)
  local crescent       = phasis_on_or_before(date, ANKARA)
  local elapsed_months = basic.round_half_to_even(
    (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH
  )
  local year  = 1 + basic.quotient(elapsed_months, 12)
  local month = 1 + elapsed_months % 12
  local day   = 1 + date - crescent
  return islamic.islamic_date(year, month, day)
end


-- === Astronomical Umm al-Qura calendar ===
-- A month starts on the day after the astronomical conjunction (new moon) occurs
-- before sunset at Mecca.  This matches the official Saudi definition for
-- the Umm al-Qura calendar more closely than the precomputed table variant.

-- True if the astronomical new moon (conjunction) occurred before sunset in
-- Mecca on the evening preceding fixed date.
-- "Before sunset" is evaluated in Mecca standard time: the conjunction must
-- fall on Mecca civil day (date-1) and before Mecca sunset on that day.
local function umalqura_astronomical_criterion(date)
  local set_std  = astro.sunset(date - 1, astro.MECCA)   -- Mecca standard time
  local set_ut   = astro.universal_from_standard(set_std, astro.MECCA)
  local conj_ut  = astro.new_moon_before(set_ut + astro.hr(1))
  local conj_std = astro.standard_from_universal(conj_ut, astro.MECCA)
  -- Conjunction must be on Mecca civil day (date-1), before Mecca sunset
  return math.floor(conj_std) == date - 1 and conj_std < set_std
end

-- Closest fixed date on or before date when an Umm al-Qura month started.
local function umalqura_astronomical_new_month_on_or_before(date)
  local moon = basic.fixed_from_moment(
    astro.lunar_phase_at_or_before(astro.NEW, date)
  )
  local age = date - moon
  -- The UmQ month starts 1 or 2 days after the conjunction.  Only go back
  -- to the previous lunation when we are on day 0 or 1 past the conjunction
  -- and the criterion has not yet fired; for age >= 2 the month start is
  -- always between moon and date.
  local tau
  if age <= 1 and not umalqura_astronomical_criterion(date) then
    tau = moon - 30
  else
    tau = moon
  end
  return basic.next(tau, function(d) return umalqura_astronomical_criterion(d) end)
end

--- Fixed date equivalent to astronomical Umm al-Qura date `i_date`.
-- @tparam table i_date Islamic date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_astronomical_islamic_umalqura(i_date)
  local month    = basic.standard_month(i_date)
  local day      = basic.standard_day(i_date)
  local year     = basic.standard_year(i_date)
  local midmonth = islamic.ISLAMIC_EPOCH
    + math.floor(
        ((year - 1) * 12 + month - 1/2)
        * astro.MEAN_SYNODIC_MONTH
      )
  return umalqura_astronomical_new_month_on_or_before(midmonth) + day - 1
end

--- Astronomical Umm al-Qura date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.astronomical_islamic_umalqura_from_fixed(date)
  local crescent       = umalqura_astronomical_new_month_on_or_before(date)
  local elapsed_months = basic.round_half_to_even(
    (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH
  )
  local year  = 1 + basic.quotient(elapsed_months, 12)
  local month = 1 + elapsed_months % 12
  local day   = 1 + date - crescent
  return islamic.islamic_date(year, month, day)
end


-- === Astronomical Easter ===

--- Date of (proposed) astronomical Easter in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed date.
function M.astronomical_easter(g_year)
  local equinox     = astro.season_in_gregorian(astro.SPRING, g_year)
  local paschal_moon = math.floor(
    astro.apparent_from_universal(
      astro.lunar_phase_at_or_after(astro.FULL, equinox),
      JERUSALEM
    )
  )
  return gregorian.kday_after(basic.SUNDAY, paschal_moon)
end


-- === Observational Hebrew calendar ===

-- Fixed date of Observational Nisan 1 occurring in Gregorian year g_year.
local function observational_hebrew_first_of_nisan(g_year)
  local equinox = astro.season_in_gregorian(astro.SPRING, g_year)
  local set     = astro.universal_from_standard(
    astro.sunset(math.floor(equinox), HEBREW_LOCATION),
    HEBREW_LOCATION
  )
  return phasis_on_or_after(
    math.floor(equinox) - (equinox < set and 14 or 13),
    HEBREW_LOCATION
  )
end

--- Fixed date equivalent to Observational Hebrew date `h_date`.
-- @tparam table h_date Hebrew date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_observational_hebrew(h_date)
  local month = basic.standard_month(h_date)
  local day   = basic.standard_day(h_date)
  local year  = basic.standard_year(h_date)
  local year1 = month >= hebrew.TISHRI and year - 1 or year
  local start = hebrew.fixed_from_hebrew(
    hebrew.hebrew_date(year1, hebrew.NISAN, 1)
  )
  local g_year  = gregorian.gregorian_year_from_fixed(start + 60)
  local new_year = observational_hebrew_first_of_nisan(g_year)
  local midmonth = new_year
    + basic.round_half_to_even(29.5 * (month - 1)) + 15
  return phasis_on_or_before(midmonth, HEBREW_LOCATION) + day - 1
end

--- Observational Hebrew date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.observational_hebrew_from_fixed(date)
  local crescent = phasis_on_or_before(date, HEBREW_LOCATION)
  local g_year   = gregorian.gregorian_year_from_fixed(date)
  local ny       = observational_hebrew_first_of_nisan(g_year)
  local new_year = date < ny
    and observational_hebrew_first_of_nisan(g_year - 1)
    or ny
  local month = 1 + basic.round_half_to_even(
    (crescent - new_year) / 29.5
  )
  local year = basic.standard_year(hebrew.hebrew_from_fixed(new_year))
    + (month >= hebrew.TISHRI and 1 or 0)
  local day = date - crescent + 1
  return hebrew.hebrew_date(year, month, day)
end

-- Fixed date of Classical Passover Eve (Nisan 14) in Gregorian year g_year.
local function classical_passover_eve(g_year) -- luacheck: ignore
  return observational_hebrew_first_of_nisan(g_year) + 13
end


-- === Month length and early-month helpers ===

-- Length of lunar month based on observability at location, which includes date.
local function month_length(date, location)
  local moon = phasis_on_or_after(date + 1, location)
  local prev = phasis_on_or_before(date, location)
  return moon - prev
end

-- True if date at location is in a month that was forced to start early.
local function early_month(date, location)
  local start = phasis_on_or_before(date, location)
  local prev  = start - 15
  return (date - start) >= 30
    or month_length(prev, location) > 30
    or (month_length(prev, location) == 30
        and early_month(prev, location))
end

-- Fixed date equivalent to Observational Islamic i_date; months never longer than 30 days.
local function alt_fixed_from_observational_islamic(i_date) -- luacheck: ignore
  local month = basic.standard_month(i_date)
  local day   = basic.standard_day(i_date)
  local year  = basic.standard_year(i_date)
  local midmonth = islamic.ISLAMIC_EPOCH
    + math.floor(
        ((year - 1) * 12 + month - 1/2)
        * astro.MEAN_SYNODIC_MONTH
      )
  local moon  = phasis_on_or_before(midmonth, ISLAMIC_LOCATION)
  local result = moon + day - 1
  return early_month(midmonth, ISLAMIC_LOCATION) and result - 1 or result
end

-- Observational Islamic date for fixed date; months never longer than 30 days.
local function alt_observational_islamic_from_fixed(date) -- luacheck: ignore
  local early      = early_month(date, ISLAMIC_LOCATION)
  local long       = early and month_length(date, ISLAMIC_LOCATION) > 29
  local date_prime = long and date + 1 or date
  local moon       = phasis_on_or_before(date_prime, ISLAMIC_LOCATION)
  local elapsed_months = basic.round_half_to_even(
    (moon - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH
  )
  local year  = 1 + basic.quotient(elapsed_months, 12)
  local month = 1 + elapsed_months % 12
  local day   = date_prime - moon + (early and not long and 2 or 1)
  return islamic.islamic_date(year, month, day)
end

-- Observational Hebrew date for fixed date; months never longer than 30 days.
local function alt_observational_hebrew_from_fixed(date) -- luacheck: ignore
  local early      = early_month(date, HEBREW_LOCATION)
  local long       = early and month_length(date, HEBREW_LOCATION) > 29
  local date_prime = long and date + 1 or date
  local moon       = phasis_on_or_before(date_prime, HEBREW_LOCATION)
  local g_year     = gregorian.gregorian_year_from_fixed(date_prime)
  local ny         = observational_hebrew_first_of_nisan(g_year)
  local new_year   = date_prime < ny
    and observational_hebrew_first_of_nisan(g_year - 1)
    or ny
  local month = 1 + basic.round_half_to_even((moon - new_year) / 29.5)
  local year  = basic.standard_year(hebrew.hebrew_from_fixed(new_year))
    + (month >= hebrew.TISHRI and 1 or 0)
  local day   = date_prime - moon + (early and not long and 2 or 1)
  return hebrew.hebrew_date(year, month, day)
end

-- Fixed date equivalent to Observational Hebrew h_date; months never longer than 30 days.
local function alt_fixed_from_observational_hebrew(h_date) -- luacheck: ignore
  local month = basic.standard_month(h_date)
  local day   = basic.standard_day(h_date)
  local year  = basic.standard_year(h_date)
  local year1 = month >= hebrew.TISHRI and year - 1 or year
  local start = hebrew.fixed_from_hebrew(
    hebrew.hebrew_date(year1, hebrew.NISAN, 1)
  )
  local g_year   = gregorian.gregorian_year_from_fixed(start + 60)
  local new_year = observational_hebrew_first_of_nisan(g_year)
  local midmonth = new_year
    + basic.round_half_to_even(29.5 * (month - 1)) + 15
  local moon   = phasis_on_or_before(midmonth, HEBREW_LOCATION)
  local result = moon + day - 1
  return early_month(midmonth, HEBREW_LOCATION) and result - 1 or result
end


-- === Samaritan calendar ===

-- Fixed date of start of the Samaritan Entry Era (March 15, 1639 BCE Julian).
local SAMARITAN_EPOCH = julian.fixed_from_julian(
  julian.julian_date(julian.bce(1639), julian.MARCH, 15)
)

-- Universal time of true noon on date at Samaritan location.
local function samaritan_noon(date)
  return astro.midday(date, SAMARITAN_LOCATION)
end

-- Fixed date of first new moon after UT moment tee (modern calculation).
local function samaritan_new_moon_after(tee)
  return math.ceil(
    astro.apparent_from_universal(
      astro.new_moon_at_or_after(tee), SAMARITAN_LOCATION
    ) - astro.hr(12)
  )
end

-- Fixed date of last new moon before UT moment tee (modern calculation).
local function samaritan_new_moon_at_or_before(tee)
  return math.ceil(
    astro.apparent_from_universal(
      astro.new_moon_before(tee), SAMARITAN_LOCATION
    ) - astro.hr(12)
  )
end

-- Fixed date of Samaritan New Year on or before fixed date.
local function samaritan_new_year_on_or_before(date)
  local g_year = gregorian.gregorian_year_from_fixed(date)
  local dates = {}
  for _, d in ipairs(julian.julian_in_gregorian(julian.MARCH, 11, g_year - 1)) do
    dates[#dates + 1] = d
  end
  for _, d in ipairs(julian.julian_in_gregorian(julian.MARCH, 11, g_year)) do
    dates[#dates + 1] = d
  end
  dates[#dates + 1] = date + 1
  local n = basic.final(
    0,
    function(i)
      return samaritan_new_moon_after(samaritan_noon(dates[i + 1])) <= date
    end
  )
  return samaritan_new_moon_after(samaritan_noon(dates[n + 1]))
end

--- Samaritan date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.samaritan_from_fixed(date)
  local moon     = samaritan_new_moon_at_or_before(samaritan_noon(date))
  local new_year = samaritan_new_year_on_or_before(moon)
  local month    = 1 + basic.round_half_to_even((moon - new_year) / 29.5)
  local year     = basic.round_half_to_even(
    (new_year - SAMARITAN_EPOCH) / 365.25
  ) + math.ceil((month - 5) / 8)
  local day = date - moon + 1
  return hebrew.hebrew_date(year, month, day)
end

--- Fixed date of Samaritan date `s_date`.
-- @tparam table s_date Samaritan date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_samaritan(s_date)
  local month = basic.standard_month(s_date)
  local day   = basic.standard_day(s_date)
  local year  = basic.standard_year(s_date)
  local ny = samaritan_new_year_on_or_before(
    math.floor(
      SAMARITAN_EPOCH + 50 + 365.25 * (year - math.ceil((month - 5) / 8))
    )
  )
  local nm = samaritan_new_moon_at_or_before(ny + 29.5 * (month - 1) + 15)
  return nm + day - 1
end


-- === Exports ===

--- True if Babylonian year `b_year` is a leap year.
-- @function babylonian_leap_year
-- @tparam number b_year Babylonian year.
-- @treturn boolean
M.babylonian_leap_year = babylonian_leap_year

--- Closest fixed date on or before `date` when crescent moon first became visible at `location`.
-- @function phasis_on_or_before
-- @tparam number date     Fixed date.
-- @tparam table  location Location table.
-- @treturn number Fixed date.
M.phasis_on_or_before = phasis_on_or_before

--- Closest fixed date on or after `date` when crescent moon first became visible at `location`.
-- @function phasis_on_or_after
-- @tparam number date     Fixed date.
-- @tparam table  location Location table.
-- @treturn number Fixed date.
M.phasis_on_or_after = phasis_on_or_after

--- True if crescent moon is likely visible on the eve of `date` at `location` (Shaukat criterion).
-- @function shaukat_criterion
-- @tparam number date     Fixed date.
-- @tparam table  location Location table.
-- @treturn boolean
M.shaukat_criterion = shaukat_criterion

--- True if crescent moon is possibly visible on the eve of `date` at `location` (Yallop criterion).
-- @function yallop_criterion
-- @tparam number date     Fixed date.
-- @tparam table  location Location table.
-- @treturn boolean
M.yallop_criterion = yallop_criterion


return M
