--- Persian calendar conversions (astronomical and arithmetic).
-- 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-persian
-- @release 0.1 2026-07-19

local M = {}

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


-- === Epoch and location ===

-- Fixed date of start of the Persian calendar (March 19, 622 CE Julian).
local PERSIAN_EPOCH = julian.fixed_from_julian(
  julian.julian_date(julian.ce(622), julian.MARCH, 19)
)

-- Location of Tehran, Iran.
local TEHRAN = astro.location(35.68, 51.42, astro.mt(1100), astro.hr(3 + 1/2))


-- === Date constructor ===

-- Persian date constructor.
local function persian_date(year, month, day)
  return {year, month, day}
end


-- === Astronomical Persian calendar ===

-- Universal time of true noon on fixed date in Tehran.
local function midday_in_tehran(date)
  return astro.midday(date, TEHRAN)
end

-- Fixed date of Astronomical Persian New Year on or before fixed date.
local function persian_new_year_on_or_before(date)
  local approx = astro.estimate_prior_solar_longitude(
    astro.SPRING, midday_in_tehran(date)
  )
  return basic.next(
    math.floor(approx) - 1,
    function(day)
      return astro.solar_longitude(midday_in_tehran(day))
        <= astro.SPRING + 2
    end
  )
end

--- Fixed date of Astronomical Persian date `p_date`.
-- @tparam table p_date Persian date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_persian(p_date)
  local month = basic.standard_month(p_date)
  local day   = basic.standard_day(p_date)
  local year  = basic.standard_year(p_date)
  local new_year = persian_new_year_on_or_before(
    PERSIAN_EPOCH + 180
    + math.floor(
        astro.MEAN_TROPICAL_YEAR * (year > 0 and year - 1 or year)
      )
  )
  local prior_months
  if month <= 7 then
    prior_months = 31 * (month - 1)
  else
    prior_months = 30 * (month - 1) + 6
  end
  return (new_year - 1)
    + prior_months
    + day
end
local fixed_from_persian = M.fixed_from_persian

--- Astronomical Persian date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.persian_from_fixed(date)
  local new_year = persian_new_year_on_or_before(date)
  local y = 1 + basic.round_half_to_even(
    (new_year - PERSIAN_EPOCH) / astro.MEAN_TROPICAL_YEAR
  )
  local year = y > 0 and y or y - 1  -- No year zero.
  local day_of_year = 1 + date
    - fixed_from_persian(persian_date(year, 1, 1))
  local month
  if day_of_year <= 186 then
    month = math.ceil(day_of_year / 31)
  else
    month = math.ceil((day_of_year - 6) / 30)
  end
  local day = date - fixed_from_persian(persian_date(year, month, 1)) + 1
  return persian_date(year, month, day)
end


-- === Arithmetic Persian calendar ===

-- True if p_year is an arithmetic Persian leap year.
local function arithmetic_persian_leap_year(p_year)
  local y = p_year > 0 and p_year - 474 or p_year - 473
  local year = (y % 2820) + 474
  return (year * 31 - 5) % 128 < 31
end

--- Fixed date equivalent to arithmetic Persian date `p_date`.
-- @tparam table p_date Persian date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_arithmetic_persian(p_date)
  local day    = basic.standard_day(p_date)
  local month  = basic.standard_month(p_date)
  local p_year = basic.standard_year(p_date)
  local y    = p_year > 0 and p_year - 474 or p_year - 473
  local year = (y % 2820) + 474
  local prior_months
  if month <= 7 then
    prior_months = 31 * (month - 1)
  else
    prior_months = 30 * (month - 1) + 6
  end
  return (PERSIAN_EPOCH - 1)
    + 1029983 * basic.quotient(y, 2820)
    + 365 * (year - 1)
    + basic.quotient(31 * year - 5, 128)
    + prior_months
    + day
end
local fixed_from_arithmetic_persian = M.fixed_from_arithmetic_persian

-- Persian year corresponding to fixed date.
local function arithmetic_persian_year_from_fixed(date)
  local d0 = date - fixed_from_arithmetic_persian(persian_date(475, 1, 1))
  local n2820 = basic.quotient(d0, 1029983)
  local d1 = d0 % 1029983
  local y2820
  if d1 == 1029982 then
    y2820 = 2820
  else
    y2820 = basic.quotient(128 * d1 + 46878, 46751)
  end
  local year = 474 + 2820 * n2820 + y2820
  return year > 0 and year or year - 1
end

--- Arithmetic Persian date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.arithmetic_persian_from_fixed(date)
  local year = arithmetic_persian_year_from_fixed(date)
  local day_of_year = 1 + date
    - fixed_from_arithmetic_persian(persian_date(year, 1, 1))
  local month
  if day_of_year <= 186 then
    month = math.ceil(day_of_year / 31)
  else
    month = math.ceil((day_of_year - 6) / 30)
  end
  local day = date
    - fixed_from_arithmetic_persian(persian_date(year, month, 1)) + 1
  return persian_date(year, month, day)
end


-- === Nowruz ===

-- Fixed date of Persian New Year (Nowruz) in Gregorian year g_year.
--- Fixed date of Nowruz (Persian New Year) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn number Fixed date.
function M.nowruz(g_year)
  local persian_year = 1 + g_year
    - gregorian.gregorian_year_from_fixed(PERSIAN_EPOCH)
  local y = persian_year <= 0 and persian_year - 1 or persian_year
  return fixed_from_persian(persian_date(y, 1, 1))
end

--- True if arithmetic Persian year `p_year` is a leap year.
-- @function arithmetic_persian_leap_year
-- @tparam number p_year Persian year.
-- @treturn boolean
M.arithmetic_persian_leap_year = arithmetic_persian_leap_year


return M
