--- Baha'i calendar conversions (arithmetic 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-bahai
-- @release 0.1 2026-07-19

local M = {}

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


-- === Constants ===

-- Fixed date of start of Baha'i calendar (March 21, 1844 Gregorian).
local BAHAI_EPOCH = gregorian.fixed_from_gregorian(
  gregorian.gregorian_date(1844, gregorian.MARCH, 21)
)

-- Signifies the intercalary period of 4 or 5 days.
local AYYAM_I_HA = 0

-- Location of Tehran for astronomical Baha'i calendar.
local BAHAI_LOCATION = astro.location(
  35.696111, 51.423056, astro.mt(0), astro.hr(3 + 1/2)
)


-- === Date constructor and accessors ===

-- Baha'i date constructor.
local function bahai_date(major, cycle, year, month, day)
  return {major, cycle, year, month, day}
end

-- Baha'i date accessors.
local function bahai_major(date) return date[1] end
local function bahai_cycle(date) return date[2] end
local function bahai_year(date)  return date[3] end
local function bahai_month(date) return date[4] end
local function bahai_day(date)   return date[5] end


-- === Arithmetic Baha'i calendar ===

--- Fixed date equivalent to the Baha'i date `b_date`.
-- @tparam table b_date Baha'i date {major, cycle, year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_bahai(b_date)
  local major = bahai_major(b_date)
  local cycle = bahai_cycle(b_date)
  local year  = bahai_year(b_date)
  local month = bahai_month(b_date)
  local day   = bahai_day(b_date)
  local g_year = 361 * (major - 1)
    + 19 * (cycle - 1)
    + year - 1
    + gregorian.gregorian_year_from_fixed(BAHAI_EPOCH)
  local prior_months
  if month == AYYAM_I_HA then
    prior_months = 342
  elseif month == 19 then
    if gregorian.gregorian_leap_year(g_year + 1) then
      prior_months = 347
    else
      prior_months = 346
    end
  else
    prior_months = 19 * (month - 1)
  end
  return gregorian.fixed_from_gregorian(
    gregorian.gregorian_date(g_year, gregorian.MARCH, 20)
  ) + prior_months + day
end
local fixed_from_bahai = M.fixed_from_bahai

--- Baha'i date {major, cycle, year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {major, cycle, year, month, day}
function M.bahai_from_fixed(date)
  local g_year = gregorian.gregorian_year_from_fixed(date)
  local start  = gregorian.gregorian_year_from_fixed(BAHAI_EPOCH)
  local years  = g_year - start
    - (date <= gregorian.fixed_from_gregorian(
        gregorian.gregorian_date(g_year, gregorian.MARCH, 20)
      ) and 1 or 0)
  local major = 1 + basic.quotient(years, 361)
  local cycle = 1 + basic.quotient(years % 361, 19)
  local year  = 1 + (years % 19)
  local days  = date - fixed_from_bahai(bahai_date(major, cycle, year, 1, 1))
  local month
  if date >= fixed_from_bahai(bahai_date(major, cycle, year, 19, 1)) then
    month = 19
  elseif date >= fixed_from_bahai(
    bahai_date(major, cycle, year, AYYAM_I_HA, 1)
  ) then
    month = AYYAM_I_HA
  else
    month = 1 + basic.quotient(days, 19)
  end
  local day = date + 1
    - fixed_from_bahai(bahai_date(major, cycle, year, month, 1))
  return bahai_date(major, cycle, year, month, day)
end

--- Fixed date of arithmetic Bahá'í New Year (always March 21) in Gregorian year `g_year`.
-- For the astronomical version based on the vernal equinox, use `naw_ruz`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed date.
function M.bahai_new_year(g_year)
  return gregorian.fixed_from_gregorian(
    gregorian.gregorian_date(g_year, gregorian.MARCH, 21)
  )
end


-- === Astronomical Baha'i calendar ===

-- Universal time of sunset on fixed date in BAHAI_LOCATION.
local function bahai_sunset(date)
  return astro.universal_from_standard(
    astro.sunset(date, BAHAI_LOCATION),
    BAHAI_LOCATION
  )
end

-- Fixed date of astronomical Baha'i New Year on or before fixed date.
local function astro_bahai_new_year_on_or_before(date)
  local approx = astro.estimate_prior_solar_longitude(
    astro.SPRING, bahai_sunset(date)
  )
  return basic.next(
    math.floor(approx) - 1,
    function(day)
      return astro.solar_longitude(bahai_sunset(day))
        <= astro.SPRING + 2
    end
  )
end

--- Fixed date of astronomical Baha'i date `b_date`.
-- @tparam table b_date Baha'i date {major, cycle, year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_astro_bahai(b_date)
  local major = bahai_major(b_date)
  local cycle = bahai_cycle(b_date)
  local year  = bahai_year(b_date)
  local month = bahai_month(b_date)
  local day   = bahai_day(b_date)
  local years = 361 * (major - 1) + 19 * (cycle - 1) + year
  if month == 19 then
    return astro_bahai_new_year_on_or_before(
      BAHAI_EPOCH + math.floor(astro.MEAN_TROPICAL_YEAR * (years + 1/2))
    ) - 20 + day
  elseif month == AYYAM_I_HA then
    return astro_bahai_new_year_on_or_before(
      BAHAI_EPOCH + math.floor(astro.MEAN_TROPICAL_YEAR * (years - 1/2))
    ) + 341 + day
  else
    return astro_bahai_new_year_on_or_before(
      BAHAI_EPOCH + math.floor(astro.MEAN_TROPICAL_YEAR * (years - 1/2))
    ) + 19 * (month - 1) + day - 1
  end
end
local fixed_from_astro_bahai = M.fixed_from_astro_bahai

--- Astronomical Baha'i date {major,cycle,year,month,day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {major, cycle, year, month, day}
function M.astro_bahai_from_fixed(date)
  local new_year = astro_bahai_new_year_on_or_before(date)
  local years    = basic.round_half_to_even(
    (new_year - BAHAI_EPOCH) / astro.MEAN_TROPICAL_YEAR
  )
  local major = 1 + basic.quotient(years, 361)
  local cycle = 1 + basic.quotient(years % 361, 19)
  local year  = 1 + (years % 19)
  local days  = date - new_year
  local month
  if date >= fixed_from_astro_bahai(
    bahai_date(major, cycle, year, 19, 1)
  ) then
    month = 19
  elseif date >= fixed_from_astro_bahai(
    bahai_date(major, cycle, year, AYYAM_I_HA, 1)
  ) then
    month = AYYAM_I_HA
  else
    month = 1 + basic.quotient(days, 19)
  end
  local day = date + 1
    - fixed_from_astro_bahai(bahai_date(major, cycle, year, month, 1))
  return bahai_date(major, cycle, year, month, day)
end


-- === Baha'i holidays ===

--- Fixed date of Bahá'í New Year (Naw-Rúz) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed date.
function M.naw_ruz(g_year)
  return astro_bahai_new_year_on_or_before(
    gregorian.gregorian_new_year(g_year + 1)
  )
end
local naw_ruz = M.naw_ruz

--- Fixed date of the Feast of Riḍván in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed date.
function M.feast_of_ridvan(g_year)
  return naw_ruz(g_year) + 31
end

--- Fixed date of the Birthday of the Báb in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed date.
function M.birth_of_the_bab(g_year)
  local ny   = naw_ruz(g_year)
  local set1 = bahai_sunset(ny)
  local m1   = astro.new_moon_at_or_after(set1)
  local m8   = astro.new_moon_at_or_after(m1 + 190)
  local day  = basic.fixed_from_moment(m8)
  local set8 = bahai_sunset(day)
  if m8 < set8 then
    return day + 1
  else
    return day + 2
  end
end


-- === Exports ===

--- Intercalary month number (0) inserted before the last month of the year.
M.AYYAM_I_HA = AYYAM_I_HA

return M
