--- Modern Hindu solar and lunar calendar conversions (traditional and astronomical).
-- 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-modern-hindu
-- @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 old_hindu = require("calendrica-old-hindu")


-- === Hindu astronomical constants ===

-- Mean length of Hindu sidereal year in days.
local HINDU_SIDEREAL_YEAR = 365 + 279457/1080000

-- Fixed date of Hindu creation.
local HINDU_CREATION = old_hindu.HINDU_EPOCH
  - 1955880000 * HINDU_SIDEREAL_YEAR

-- Mean length of Hindu sidereal month in days.
local HINDU_SIDEREAL_MONTH = 27 + 4644439/14438334

-- Mean time from new moon to new moon.
local HINDU_SYNODIC_MONTH = 29 + 7087771/13358334

-- Time from aphelion to aphelion.
local HINDU_ANOMALISTIC_YEAR = 1577917828000 / (4320000000 - 387)

-- Time from apogee to apogee, with bija correction.
local HINDU_ANOMALISTIC_MONTH = 1577917828 / (57753336 - 488199)

-- Years from Kali Yuga until Saka era.
local HINDU_SOLAR_ERA = 3179

-- Years from Kali Yuga until Vikrama era.
local HINDU_LUNAR_ERA = 3044

-- Location of Ujjain.
local UJJAIN = astro.location(
  astro.angle(23, 9, 0), astro.angle(75, 46, 6),
  astro.mt(0), astro.hr(5 + 461/9000)
)

-- Location (Ujjain) for determining Hindu calendar.
local HINDU_LOCATION = UJJAIN


-- === Sidereal start (lazy, computed once) ===

local mesha_samkranti  -- forward declaration (defined after hindu_solar_longitude_at_or_after)

local _sidereal_start
-- Sidereal start angle: precession at Mesha Samkranti in 285 CE.
local function sidereal_start()
  if not _sidereal_start then
    _sidereal_start = astro.precession(
      astro.universal_from_local(
        mesha_samkranti(julian.ce(285)),
        HINDU_LOCATION
      )
    )
  end
  return _sidereal_start
end


-- === Hindu date constructor and accessors ===

-- Hindu lunar date constructor.
local function hindu_lunar_date(year, month, leap_month, day, leap_day)
  return {year, month, leap_month, day, leap_day}
end

-- Hindu lunar date accessors.
local function hindu_lunar_year(date)        return date[1] end
local function hindu_lunar_month(date)       return date[2] end
local function hindu_lunar_leap_month(date)  return date[3] end
local function hindu_lunar_day(date)         return date[4] end
local function hindu_lunar_leap_day(date)    return date[5] end


-- === Hindu sine table ===

-- Simulated Hindu sine table for entry (angle as multiple of 225 arcminutes).
local function hindu_sine_table(entry)
  local exact = 3438 * astro.sin_degrees(entry * astro.angle(0, 225, 0))
  local error = 0.215 * basic.sign(exact) * basic.sign(math.abs(exact) - 1716)
  return math.floor(exact + error + 0.5) / 3438
end

-- Linear interpolation of Hindu sine for theta.
local function hindu_sine(theta)
  local entry    = theta / astro.angle(0, 225, 0)
  local fraction = entry % 1
  return fraction * hindu_sine_table(math.ceil(entry))
       + (1 - fraction) * hindu_sine_table(math.floor(entry))
end

-- Inverse of Hindu sine function.
local function hindu_arcsin(amp)
  if amp < 0 then
    return -hindu_arcsin(-amp)
  end
  local pos   = basic.next(0, function(k) return amp <= hindu_sine_table(k) end)
  local below = hindu_sine_table(pos - 1)
  return astro.angle(0, 225, 0) * (
    pos - 1 + (amp - below) / (hindu_sine_table(pos) - below)
  )
end


-- === Hindu mean and true position ===

-- Position in degrees at moment tee in uniform circular orbit of period days.
local function hindu_mean_position(tee, period)
  return 360 * ((tee - HINDU_CREATION) / period % 1)
end

-- Longitudinal position at moment tee in epicyclic orbit.
local function hindu_true_position(tee, period, size, anomalistic, change)
  local lambda     = hindu_mean_position(tee, period)
  local offset     = hindu_sine(hindu_mean_position(tee, anomalistic))
  local contraction = math.abs(offset) * change * size
  local equation   = hindu_arcsin(offset * (size - contraction))
  return (lambda - equation) % 360
end

-- Solar longitude at moment tee (Hindu sidereal).
local function hindu_solar_longitude(tee)
  return hindu_true_position(
    tee, HINDU_SIDEREAL_YEAR, 14/360,
    HINDU_ANOMALISTIC_YEAR, 1/42
  )
end

-- Zodiacal sign of the sun (1..12) at moment tee.
local function hindu_zodiac(tee)
  return 1 + basic.quotient(hindu_solar_longitude(tee), 30)
end

-- Lunar longitude at moment tee (Hindu sidereal).
local function hindu_lunar_longitude(tee)
  return hindu_true_position(
    tee, HINDU_SIDEREAL_MONTH, 32/360,
    HINDU_ANOMALISTIC_MONTH, 1/96
  )
end

--- Longitudinal distance between sun and moon in degrees at moment `tee`.
-- Used as the angular function for invert_angular in lunar_day_at_or_after.
-- @tparam number tee Moment.
-- @treturn number Phase angle in degrees [0, 360).
function M.hindu_lunar_phase(tee)
  return (hindu_lunar_longitude(tee) - hindu_solar_longitude(tee)) % 360
end
local hindu_lunar_phase = M.hindu_lunar_phase

-- Phase of moon (tithi, 1..30) at moment tee.
local function hindu_lunar_day_from_moment(tee)
  return 1 + basic.quotient(hindu_lunar_phase(tee), 12)
end

-- Approximate moment of last new moon preceding tee.
local function hindu_new_moon_before(tee)
  local varepsilon = 2^-1000
  local tau = tee - (1/360) * hindu_lunar_phase(tee) * HINDU_SYNODIC_MONTH
  return basic.binary_search(
    tau - 1, math.min(tee, tau + 1),
    function(x) return hindu_lunar_phase(x) < 180 end,
    function(l, u)
      return hindu_zodiac(l) == hindu_zodiac(u) or (u - l) < varepsilon
    end
  )
end

-- Moment lunar day k begins at or after tee (k may be fractional).
local function hindu_lunar_day_at_or_after(k, tee)
  local phase = (k - 1) * 12
  local tau   = tee + (1/360)
    * ((phase - hindu_lunar_phase(tee)) % 360)
    * HINDU_SYNODIC_MONTH
  local a = math.max(tee, tau - 2)
  local b = tau + 2
  return basic.invert_angular(
    hindu_lunar_phase, phase, basic.interval_closed(a, b)
  )
end

-- Hindu solar year at moment tee.
local function hindu_calendar_year(tee)
  return basic.round_half_to_even(
    (tee - old_hindu.HINDU_EPOCH) / HINDU_SIDEREAL_YEAR
    - hindu_solar_longitude(tee) / 360
  )
end


-- === Equation of time and sunrise helpers ===

-- Sidereal daily motion of sun on date.
local function hindu_daily_motion(date)
  local mean_motion   = 360 / HINDU_SIDEREAL_YEAR
  local anomaly       = hindu_mean_position(date, HINDU_ANOMALISTIC_YEAR)
  local epicycle      = 14/360 - math.abs(hindu_sine(anomaly)) / 1080
  local entry         = basic.quotient(anomaly, astro.angle(0, 225, 0))
  local sine_table_step = hindu_sine_table(entry + 1) - hindu_sine_table(entry)
  local factor        = -3438/225 * sine_table_step * epicycle
  return mean_motion * (1 + factor)
end

-- Hindu tropical longitude on fixed date.
local function hindu_tropical_longitude(date)
  local days = date - old_hindu.HINDU_EPOCH
  local precession = 27 - math.abs(
    108 * basic.mod3(600/1577917828 * days - 1/4, -1/2, 1/2)
  )
  return (hindu_solar_longitude(date) - precession) % 360
end

-- Tabulated speed of rising of current zodiacal sign on date.
local function hindu_rising_sign(date)
  local i = basic.quotient(hindu_tropical_longitude(date), 30)
  local table = {1670/1800, 1795/1800, 1935/1800,
                 1935/1800, 1795/1800, 1670/1800}
  return table[i % 6 + 1]
end

-- Difference between solar and sidereal day on date.
local function hindu_solar_sidereal_difference(date)
  return hindu_daily_motion(date) * hindu_rising_sign(date)
end

-- Difference between right and oblique ascension of sun on date at location.
local function hindu_ascensional_difference(date, location)
  local sin_delta    = (1397/3438) * hindu_sine(hindu_tropical_longitude(date))
  local phi          = astro.latitude(location)
  local diurnal_radius = hindu_sine(90 + hindu_arcsin(sin_delta))
  local tan_phi      = hindu_sine(phi) / hindu_sine(90 + phi)
  local earth_sine   = sin_delta * tan_phi
  return hindu_arcsin(-(earth_sine / diurnal_radius))
end

-- Time from true to mean midnight of date (gross approximation).
local function hindu_equation_of_time(date)
  local offset = hindu_sine(hindu_mean_position(date, HINDU_ANOMALISTIC_YEAR))
  local equation_sun = offset * astro.angle(57, 18, 0)
    * (14/360 - math.abs(offset) / 1080)
  return (hindu_daily_motion(date) / 360)
    * (equation_sun / 360)
    * HINDU_SIDEREAL_YEAR
end

-- Sunrise at HINDU_LOCATION on date (traditional).
local function hindu_sunrise(date)
  return date + astro.hr(6)
    + (astro.longitude(UJJAIN) - astro.longitude(HINDU_LOCATION)) / 360
    - hindu_equation_of_time(date)
    + (1577917828/1582237828 / 360)
      * (hindu_ascensional_difference(date, HINDU_LOCATION)
         + (1/4) * hindu_solar_sidereal_difference(date))
end

-- Sunset at HINDU_LOCATION on date (traditional).
local function hindu_sunset(date)
  return date + astro.hr(18)
    + (astro.longitude(UJJAIN) - astro.longitude(HINDU_LOCATION)) / 360
    - hindu_equation_of_time(date)
    + (1577917828/1582237828 / 360)
      * (-hindu_ascensional_difference(date, HINDU_LOCATION)
         + (3/4) * hindu_solar_sidereal_difference(date))
end

-- Hindu local time of temporal moment tee.
local function hindu_standard_from_sundial(tee)
  local date = basic.fixed_from_moment(tee)
  local time = basic.time_from_moment(tee)
  local q    = math.floor(4 * time)
  local a, b
  if q == 0 then
    a = hindu_sunset(date - 1)
    b = hindu_sunrise(date)
  elseif q == 3 then
    a = hindu_sunset(date)
    b = hindu_sunrise(date + 1)
  else
    a = hindu_sunrise(date)
    b = hindu_sunset(date)
  end
  local offset
  if q == 3 then      offset = astro.hr(18)
  elseif q == 0 then  offset = astro.hr(-6)
  else                offset = astro.hr(6)
  end
  return a + 2 * (b - a) * (time - offset)
end

-- Astronomical sunrise at HINDU_LOCATION on date (Lahiri; rounded to minute).
local function alt_hindu_sunrise(date)
  local rise = astro.dawn(date, HINDU_LOCATION, astro.angle(0, 47, 0))
  return math.floor(rise * 24 * 60 + 0.5) / 24 / 60
end

-- Geometrical sunset at HINDU_LOCATION on date (astronomical).
local function astro_hindu_sunset(date)
  return astro.dusk(date, HINDU_LOCATION, 0)
end


-- === Hindu solar calendar ===

--- Hindu (Orissa) solar date {year, month, day} equivalent to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.hindu_solar_from_fixed(date)
  local critical = hindu_sunrise(date + 1)
  local month    = hindu_zodiac(critical)
  local year     = hindu_calendar_year(critical) - HINDU_SOLAR_ERA
  local approx   = date - 3
    - (math.floor(hindu_solar_longitude(critical)) % 30)
  local start = basic.next(
    approx,
    function(i) return hindu_zodiac(hindu_sunrise(i + 1)) == month end
  )
  local day = date - start + 1
  return old_hindu.hindu_solar_date(year, month, day)
end

--- Fixed date corresponding to Hindu solar date `s_date` (Saka era; Orissa rule).
-- @tparam table s_date Hindu solar date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_hindu_solar(s_date)
  local month = basic.standard_month(s_date)
  local day   = basic.standard_day(s_date)
  local year  = basic.standard_year(s_date)
  local start = math.floor(
    (year + HINDU_SOLAR_ERA + (month - 1) / 12)
    * HINDU_SIDEREAL_YEAR
  ) + old_hindu.HINDU_EPOCH
  return day - 1 + basic.next(
    start - 3,
    function(d) return hindu_zodiac(hindu_sunrise(d + 1)) == month end
  )
end


-- === Hindu lunar calendar (traditional) ===

--- Hindu lunar date {year,month,leap_month,day,leap_day} equivalent to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, leap_month, day, leap_day}
function M.hindu_lunar_from_fixed(date)
  local critical      = hindu_sunrise(date)
  local day           = hindu_lunar_day_from_moment(critical)
  local leap_day      = day == hindu_lunar_day_from_moment(
                          hindu_sunrise(date - 1)
                        )
  local last_new_moon = hindu_new_moon_before(critical)
  local next_new_moon = hindu_new_moon_before(math.floor(last_new_moon) + 35)
  local solar_month   = hindu_zodiac(last_new_moon)
  local leap_month    = solar_month == hindu_zodiac(next_new_moon)
  local month         = basic.amod(solar_month + 1, 12)
  local year          = hindu_calendar_year(
    month <= 2 and date + 180 or date
  ) - HINDU_LUNAR_ERA
  return hindu_lunar_date(year, month, leap_month, day, leap_day)
end
local hindu_lunar_from_fixed = M.hindu_lunar_from_fixed

--- Fixed date corresponding to Hindu lunar date `l_date`.
-- @tparam table l_date Hindu lunar date {year, month, leap_month, day, leap_day}.
-- @treturn number Fixed date.
function M.fixed_from_hindu_lunar(l_date)
  local year       = hindu_lunar_year(l_date)
  local month      = hindu_lunar_month(l_date)
  local leap_month = hindu_lunar_leap_month(l_date)
  local day        = hindu_lunar_day(l_date)
  local leap_day   = hindu_lunar_leap_day(l_date)
  local approx     = old_hindu.HINDU_EPOCH
    + HINDU_SIDEREAL_YEAR * (year + HINDU_LUNAR_ERA + (month - 1) / 12)
  local s = math.floor(
    approx - HINDU_SIDEREAL_YEAR
    * basic.mod3(
        hindu_solar_longitude(approx) / 360 - (month - 1) / 12,
        -1/2, 1/2
      )
  )
  local k = hindu_lunar_day_from_moment(s + astro.hr(6))
  local mid = hindu_lunar_from_fixed(s - 15)
  local est
  if k > 3 and k < 27 then
    est = s - day + k
  elseif hindu_lunar_month(mid) ~= month
    or (hindu_lunar_leap_month(mid) and not leap_month)
  then
    est = s - day + basic.mod3(k, -15, 15)
  else
    est = s - day + basic.mod3(k, 15, 45)
  end
  local tau = est - basic.mod3(
    hindu_lunar_day_from_moment(est + astro.hr(6)) - day,
    -15, 15
  )
  local result = basic.next(
    tau - 1,
    function(d)
      local ld = hindu_lunar_day_from_moment(hindu_sunrise(d))
      return ld == day or ld == basic.amod(day + 1, 30)
    end
  )
  return leap_day and result + 1 or result
end
local fixed_from_hindu_lunar = M.fixed_from_hindu_lunar


-- === Hindu lunar calendar (full-moon scheme) ===

-- Hindu lunar date (full-moon scheme) equivalent to fixed date.
local function hindu_fullmoon_from_fixed(date)
  local l_date     = hindu_lunar_from_fixed(date)
  local year       = hindu_lunar_year(l_date)
  local month      = hindu_lunar_month(l_date)
  local leap_month = hindu_lunar_leap_month(l_date)
  local day        = hindu_lunar_day(l_date)
  local leap_day   = hindu_lunar_leap_day(l_date)
  local m = day >= 16
    and hindu_lunar_month(hindu_lunar_from_fixed(date + 20))
    or month
  return hindu_lunar_date(year, m, leap_month, day, leap_day)
end

-- True if Hindu lunar month l_month in l_year is expunged.
local function hindu_expunged(l_year, l_month)
  return l_month ~= hindu_lunar_month(
    hindu_lunar_from_fixed(
      fixed_from_hindu_lunar(
        hindu_lunar_date(l_year, l_month, false, 15, false)
      )
    )
  )
end

-- Fixed date equivalent to Hindu lunar l_date in full-moon scheme.
local function fixed_from_hindu_fullmoon(l_date) -- luacheck: ignore
  local year       = hindu_lunar_year(l_date)
  local month      = hindu_lunar_month(l_date)
  local leap_month = hindu_lunar_leap_month(l_date)
  local day        = hindu_lunar_day(l_date)
  local leap_day   = hindu_lunar_leap_day(l_date)
  local m
  if leap_month or day <= 15 then
    m = month
  elseif hindu_expunged(year, basic.amod(month - 1, 12)) then
    m = basic.amod(month - 2, 12)
  else
    m = basic.amod(month - 1, 12)
  end
  return fixed_from_hindu_lunar(
    hindu_lunar_date(year, m, leap_month, day, leap_day)
  )
end


-- === Astronomical Hindu solar calendar ===

-- Astronomical Hindu solar year KY at moment tee.
local function astro_hindu_calendar_year(tee)
  return basic.round_half_to_even(
    (tee - old_hindu.HINDU_EPOCH) / astro.MEAN_SIDEREAL_YEAR
    - astro.sidereal_solar_longitude(tee, sidereal_start()) / 360
  )
end

-- Sidereal zodiacal sign of sun (1..12) at moment tee.
local function sidereal_zodiac(tee)
  return 1 + basic.quotient(
    astro.sidereal_solar_longitude(tee, sidereal_start()), 30
  )
end

--- Astronomical Hindu (Tamil) solar date {year, month, day} equivalent to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.astro_hindu_solar_from_fixed(date)
  local critical = astro_hindu_sunset(date)
  local month    = sidereal_zodiac(critical)
  local year     = astro_hindu_calendar_year(critical) - HINDU_SOLAR_ERA
  local approx   = date - 3
    - (math.floor(astro.sidereal_solar_longitude(critical, sidereal_start())) % 30)
  local start = basic.next(
    approx,
    function(i) return sidereal_zodiac(astro_hindu_sunset(i)) == month end
  )
  local day = date - start + 1
  return old_hindu.hindu_solar_date(year, month, day)
end

--- Fixed date corresponding to astronomical Hindu solar date `s_date` (Tamil rule; Saka era).
-- @tparam table s_date Hindu solar date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_astro_hindu_solar(s_date)
  local month = basic.standard_month(s_date)
  local day   = basic.standard_day(s_date)
  local year  = basic.standard_year(s_date)
  local approx = old_hindu.HINDU_EPOCH - 3
    + math.floor(
        (year + HINDU_SOLAR_ERA + (month - 1) / 12)
        * astro.MEAN_SIDEREAL_YEAR
      )
  local start = basic.next(
    approx,
    function(i) return sidereal_zodiac(astro_hindu_sunset(i)) == month end
  )
  return start + day - 1
end


-- === Astronomical Hindu lunar calendar ===

-- Phase of moon (tithi, 1..30) at moment tee using astronomical lunar phase.
local function astro_lunar_day_from_moment(tee)
  return 1 + basic.quotient(astro.lunar_phase(tee), 12)
end

--- Astronomical Hindu lunar date {year,month,leap_month,day,leap_day} equivalent to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, leap_month, day, leap_day}
function M.astro_hindu_lunar_from_fixed(date)
  local critical      = alt_hindu_sunrise(date)
  local day           = astro_lunar_day_from_moment(critical)
  local leap_day      = day == astro_lunar_day_from_moment(
                          alt_hindu_sunrise(date - 1)
                        )
  local last_new_moon = astro.new_moon_before(critical)
  local next_new_moon = astro.new_moon_at_or_after(critical)
  local solar_month   = sidereal_zodiac(last_new_moon)
  local leap_month    = solar_month == sidereal_zodiac(next_new_moon)
  local month         = basic.amod(solar_month + 1, 12)
  local year          = astro_hindu_calendar_year(
    month <= 2 and date + 180 or date
  ) - HINDU_LUNAR_ERA
  return hindu_lunar_date(year, month, leap_month, day, leap_day)
end
local astro_hindu_lunar_from_fixed = M.astro_hindu_lunar_from_fixed

--- Fixed date corresponding to astronomical Hindu lunar date `l_date`.
-- @tparam table l_date Hindu lunar date {year, month, leap_month, day, leap_day}.
-- @treturn number Fixed date.
function M.fixed_from_astro_hindu_lunar(l_date)
  local year       = hindu_lunar_year(l_date)
  local month      = hindu_lunar_month(l_date)
  local leap_month = hindu_lunar_leap_month(l_date)
  local day        = hindu_lunar_day(l_date)
  local leap_day   = hindu_lunar_leap_day(l_date)
  local approx     = old_hindu.HINDU_EPOCH
    + astro.MEAN_SIDEREAL_YEAR
      * (year + HINDU_LUNAR_ERA + (month - 1) / 12)
  local s = math.floor(
    approx - HINDU_SIDEREAL_YEAR
    * basic.mod3(
        astro.sidereal_solar_longitude(approx, sidereal_start()) / 360
        - (month - 1) / 12,
        -1/2, 1/2
      )
  )
  local k   = astro_lunar_day_from_moment(s + astro.hr(6))
  local mid = astro_hindu_lunar_from_fixed(s - 15)
  local est
  if k > 3 and k < 27 then
    est = s - day + k
  elseif hindu_lunar_month(mid) ~= month
    or (hindu_lunar_leap_month(mid) and not leap_month)
  then
    est = s - day + basic.mod3(k, -15, 15)
  else
    est = s - day + basic.mod3(k, 15, 45)
  end
  local tau = est - basic.mod3(
    astro_lunar_day_from_moment(est + astro.hr(6)) - day,
    -15, 15
  )
  local result = basic.next(
    tau - 1,
    function(d)
      local ld = astro_lunar_day_from_moment(alt_hindu_sunrise(d))
      return ld == day or ld == basic.amod(day + 1, 30)
    end
  )
  return leap_day and result + 1 or result
end


-- === Hindu solar longitude helpers ===

-- Hindu lunar station (nakshatra, 1..27) at sunrise on date.
local function hindu_lunar_station(date)
  local critical = hindu_sunrise(date)
  return 1 + basic.quotient(hindu_lunar_longitude(critical), astro.angle(0, 800, 0))
end

-- Moment of first time at or after tee when Hindu solar longitude is lambda degrees.
local function hindu_solar_longitude_at_or_after(lambda, tee)
  local tau = tee + HINDU_SIDEREAL_YEAR * (1/360)
    * ((lambda - hindu_solar_longitude(tee)) % 360)
  local a = math.max(tee, tau - 5)
  local b = tau + 5
  return basic.invert_angular(
    hindu_solar_longitude, lambda, basic.interval_closed(a, b)
  )
end

--- Fixed moment of Mesha Samkranti (Hindu solar new year, sun enters Aries) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed moment.
mesha_samkranti = function(g_year)
  return hindu_solar_longitude_at_or_after(
    0, gregorian.gregorian_new_year(g_year)
  )
end
M.mesha_samkranti = mesha_samkranti

-- Difference between tropical and sidereal solar longitude at moment tee.
local function ayanamsha(tee) -- luacheck: ignore
  return astro.solar_longitude(tee)
    - astro.sidereal_solar_longitude(tee, sidereal_start())
end


-- === Hindu new year ===

-- Fixed date of Hindu lunisolar new year in Gregorian year g_year.
--- Fixed date of Hindu Lunar New Year in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed date.
function M.hindu_lunar_new_year(g_year)
  local jan1     = gregorian.gregorian_new_year(g_year)
  local mina     = hindu_solar_longitude_at_or_after(330, jan1)
  local new_moon = hindu_lunar_day_at_or_after(1, mina)
  local h_day    = math.floor(new_moon)
  local critical = hindu_sunrise(h_day)
  return h_day + (
    (new_moon < critical
     or hindu_lunar_day_from_moment(hindu_sunrise(h_day + 1)) == 2)
    and 0 or 1
  )
end


-- === Hindu lunar date ordering ===

-- True if Hindu lunar date l_date1 is on or before l_date2.
local function hindu_lunar_on_or_before(l_date1, l_date2)
  local month1    = hindu_lunar_month(l_date1)
  local month2    = hindu_lunar_month(l_date2)
  local leap1     = hindu_lunar_leap_month(l_date1)
  local leap2     = hindu_lunar_leap_month(l_date2)
  local day1      = hindu_lunar_day(l_date1)
  local day2      = hindu_lunar_day(l_date2)
  local leap_day1 = hindu_lunar_leap_day(l_date1)
  local leap_day2 = hindu_lunar_leap_day(l_date2)
  local year1     = hindu_lunar_year(l_date1)
  local year2     = hindu_lunar_year(l_date2)
  return year1 < year2
    or (year1 == year2 and (
      month1 < month2
      or (month1 == month2 and (
        (leap1 and not leap2)
        or (leap1 == leap2 and (
          day1 < day2
          or (day1 == day2 and (
            not leap_day1 or leap_day2
          ))
        ))
      ))
    ))
end


-- === Hindu date occurrence and holidays ===

-- Fixed date of occurrence of l_month, l_day in l_year (leap/expunged aware).
local function hindu_date_occur(l_year, l_month, l_day)
  local lunar  = hindu_lunar_date(l_year, l_month, false, l_day, false)
  local try    = fixed_from_hindu_lunar(lunar)
  local mid    = hindu_lunar_from_fixed(l_day > 15 and try - 5 or try)
  local expunged = l_month ~= hindu_lunar_month(mid)
  local l_date = hindu_lunar_date(
    hindu_lunar_year(mid),
    hindu_lunar_month(mid),
    hindu_lunar_leap_month(mid),
    l_day, false
  )
  if expunged then
    return basic.next(try, function(d)
      return not hindu_lunar_on_or_before(
        hindu_lunar_from_fixed(d), l_date
      )
    end) - 1
  elseif l_day ~= hindu_lunar_day(hindu_lunar_from_fixed(try)) then
    return try - 1
  else
    return try
  end
end

-- List of fixed dates of occurrences of lunar month/day in Gregorian g_year.
local function hindu_lunar_holiday(l_month, l_day, g_year)
  local l_year = hindu_lunar_year(
    hindu_lunar_from_fixed(gregorian.gregorian_new_year(g_year))
  )
  local date0 = hindu_date_occur(l_year,     l_month, l_day)
  local date1 = hindu_date_occur(l_year + 1, l_month, l_day)
  return basic.list_range(
    {date0, date1},
    gregorian.gregorian_year_range(g_year)
  )
end

-- List of fixed dates of Diwali in Gregorian year g_year.
--- Fixed dates of Diwali (Hindu lunar month 8, day 1) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.diwali(g_year)
  return hindu_lunar_holiday(8, 1, g_year)
end

-- Fixed date of occurrence of tithi at sundial time tee in l_month, l_year.
local function hindu_tithi_occur(l_month, tithi, tee, l_year)
  local approx = hindu_date_occur(l_year, l_month, math.floor(tithi))
  local lunar  = hindu_lunar_day_at_or_after(tithi, approx - 2)
  local try    = basic.fixed_from_moment(lunar)
  local tee_h  = hindu_standard_from_sundial(try + tee, UJJAIN)
  if lunar <= tee_h
    or hindu_lunar_phase(
         hindu_standard_from_sundial(try + 1 + tee, UJJAIN)
       ) > 12 * tithi
  then
    return try
  else
    return try + 1
  end
end

-- List of fixed dates of tithi at sundial time tee in l_month in Gregorian g_year.
local function hindu_lunar_event(l_month, tithi, tee, g_year)
  local l_year = hindu_lunar_year(
    hindu_lunar_from_fixed(gregorian.gregorian_new_year(g_year))
  )
  local date0 = hindu_tithi_occur(l_month, tithi, tee, l_year)
  local date1 = hindu_tithi_occur(l_month, tithi, tee, l_year + 1)
  return basic.list_range(
    {date0, date1},
    gregorian.gregorian_year_range(g_year)
  )
end

--- Fixed dates of Maha Shivaratri (Night of Shiva) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.shiva(g_year)
  return hindu_lunar_event(11, 29, astro.hr(24), g_year)
end

--- Fixed dates of Rama Navami (Rama's Birthday) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.rama(g_year)
  return hindu_lunar_event(1, 9, astro.hr(12), g_year)
end


-- === Karana, yoga, sacred Wednesdays ===

-- Number (0-10) of the name of the n-th (1-60) Hindu karana.
local function karana(n) -- luacheck: ignore
  if n == 1 then
    return 0
  elseif n > 57 then
    return n - 50
  else
    return basic.amod(n - 1, 7)
  end
end

-- Hindu yoga (1..27) on date.
local function yoga(date) -- luacheck: ignore
  return 1 + math.floor(
    (hindu_solar_longitude(date) + hindu_lunar_longitude(date))
    / astro.angle(0, 800, 0) % 27
  )
end

-- List of Wednesdays within range that are day 8 of Hindu lunar months.
local function sacred_wednesdays_in_range(range)
  local result = {}
  local wed    = gregorian.kday_on_or_after(basic.WEDNESDAY, basic.begin(range))
  while basic.in_range(wed, range) do
    local h_date = hindu_lunar_from_fixed(wed)
    if hindu_lunar_day(h_date) == 8 then
      result[#result + 1] = wed
    end
    wed = wed + 7
  end
  return result
end

--- Fixed dates of Sacred Wednesdays (lunar day 8) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.sacred_wednesdays(g_year)
  return sacred_wednesdays_in_range(gregorian.gregorian_year_range(g_year))
end


-- === Exports ===

--- Moment of sunrise at HINDU_LOCATION on fixed `date` (traditional calculation).
-- @function hindu_sunrise
-- @tparam number date Fixed date.
-- @treturn number Moment.
M.hindu_sunrise = hindu_sunrise

--- Moment of sunset at HINDU_LOCATION on fixed `date` (traditional calculation).
-- @function hindu_sunset
-- @tparam number date Fixed date.
-- @treturn number Moment.
M.hindu_sunset = hindu_sunset

--- Moment of geometrical sunset at HINDU_LOCATION on fixed `date` (astronomical).
-- @function astro_hindu_sunset
-- @tparam number date Fixed date.
-- @treturn number Moment.
M.astro_hindu_sunset = astro_hindu_sunset

--- Nakshatra (lunar station, 1..27) at sunrise on fixed `date`.
-- @function hindu_lunar_station
-- @tparam number date Fixed date.
-- @treturn number Nakshatra index.
M.hindu_lunar_station = hindu_lunar_station

--- Hindu lunar date in full-moon scheme for fixed `date`.
-- @function hindu_fullmoon_from_fixed
-- @tparam number date Fixed date.
-- @treturn table Hindu lunar date {year, month, leap_month, day, leap_day}.
M.hindu_fullmoon_from_fixed = hindu_fullmoon_from_fixed

--- True if Hindu lunar month `l_month` in year `l_year` is expunged (skipped).
-- @function hindu_expunged
-- @tparam number l_year  Hindu lunar year.
-- @tparam number l_month Hindu lunar month.
-- @treturn boolean
M.hindu_expunged = hindu_expunged

--- Fixed dates of Hindu lunar `l_month`/`l_day` falling in Gregorian year `g_year`.
-- @function hindu_lunar_holiday
-- @tparam number l_month Hindu lunar month.
-- @tparam number l_day   Hindu lunar day.
-- @tparam number g_year  Gregorian year.
-- @treturn {number,...} Fixed dates.
M.hindu_lunar_holiday = hindu_lunar_holiday

return M
