--- Julian calendar conversions, Roman calendar, AUC/Olympiad helpers.
-- 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-julian
-- @release 0.1 2026-07-19

local M = {}


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


-- === Month constants (re-exported for convenience) ===
-- These mirror the Gregorian month constants since Julian months are the same.

local JANUARY = gregorian.JANUARY
local FEBRUARY = gregorian.FEBRUARY
local MARCH = gregorian.MARCH
local APRIL = gregorian.APRIL
local MAY = gregorian.MAY
local JUNE = gregorian.JUNE
local JULY = gregorian.JULY
local AUGUST = gregorian.AUGUST
local SEPTEMBER = gregorian.SEPTEMBER
local OCTOBER = gregorian.OCTOBER
local NOVEMBER = gregorian.NOVEMBER
local DECEMBER = gregorian.DECEMBER


-- === Roman calendar event constants ===

local KALENDS = 1

local NONES = 2

local IDES = 3


-- === Era helpers ===

--- Return a negative year number representing `n` BCE on the Julian calendar.
-- @tparam number n Positive year count before the common era.
-- @treturn number Negative Julian year.
function M.bce(n)
  return -n
end
local bce = M.bce

--- Return a positive year number representing `n` CE on the Julian calendar.
-- @tparam number n Year in the common era.
-- @treturn number Julian year.
function M.ce(n)
  return n
end


-- === Epoch ===

local JULIAN_EPOCH = gregorian.fixed_from_gregorian(
  gregorian.gregorian_date(0, gregorian.DECEMBER, 30)
)


-- === Date constructor ===

--- Construct a Julian date from year, month, and day.
-- @tparam number year Julian year (use bce/ce helpers).
-- @tparam number month Month (1–12).
-- @tparam number day Day of month.
-- @treturn table {year, month, day}
function M.julian_date(year, month, day)
  return {year, month, day}
end
local julian_date = M.julian_date


-- === Leap year ===

--- True if `j_year` is a leap year on the Julian calendar.
-- @tparam number j_year Julian year.
-- @treturn boolean
function M.julian_leap_year(j_year)
  if j_year > 0 then
    return j_year % 4 == 0
  else
    return j_year % 4 == 3
  end
end
local julian_leap_year = M.julian_leap_year


-- === Conversion ===

--- Fixed date equivalent to the Julian date `j_date`.
-- @tparam table j_date Julian date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_julian(j_date)
  local month = basic.standard_month(j_date)
  local day   = basic.standard_day(j_date)
  local year  = basic.standard_year(j_date)
  local y     = year < 0 and year + 1 or year  -- No year zero.
  local correction
  if month <= 2 then
    correction = 0
  elseif julian_leap_year(year) then
    correction = -1
  else
    correction = -2
  end
  return (JULIAN_EPOCH - 1)           -- Days before start of calendar.
    + 365 * (y - 1)                     -- Ordinary days since epoch.
    + basic.quotient(y - 1, 4)          -- Leap days since epoch.
    + basic.quotient(367 * month - 362, 12) -- Days in prior months this year
    + correction                        -- Correct for 28- or 29-day Feb.
    + day                               -- Days so far this month.
end
local fixed_from_julian = M.fixed_from_julian

--- Julian date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.julian_from_fixed(date)
  local approx = basic.quotient(
    4 * (date - JULIAN_EPOCH) + 1464, 1461
  )
  local year = approx <= 0 and approx - 1 or approx  -- No year 0.
  local prior_days = date - fixed_from_julian(julian_date(year, JANUARY, 1))
  local correction
  if date < fixed_from_julian(julian_date(year, MARCH, 1)) then
    correction = 0
  elseif julian_leap_year(year) then
    correction = 1
  else
    correction = 2
  end
  local month = basic.quotient(12 * (prior_days + correction) + 373, 367)
  local day   = 1 + date
    - fixed_from_julian(julian_date(year, month, 1))
  return julian_date(year, month, day)
end
local julian_from_fixed = M.julian_from_fixed


-- === Roman calendar ===

--- Construct a Roman calendar date.
-- @tparam number year Julian year.
-- @tparam number month Month (1–12).
-- @tparam number event Roman event (KALENDS, NONES, or IDES).
-- @tparam number count Count before the event.
-- @tparam boolean leap True for the leap-day variant in March of a leap year.
-- @treturn table {year, month, event, count, leap}
function M.roman_date(year, month, event, count, leap)
  return {year, month, event, count, leap}
end
local roman_date = M.roman_date

local function roman_year(date)  return date[1] end
local function roman_month(date) return date[2] end
local function roman_event(date) return date[3] end
local function roman_count(date) return date[4] end
local function roman_leap(date)  return date[5] end

-- Day of Ides in Roman month.
local function ides_of_month(month)
  if month == MARCH or month == MAY
    or month == JULY or month == OCTOBER
  then
    return 15
  else
    return 13
  end
end

-- Day of Nones in Roman month.
local function nones_of_month(month)
  return ides_of_month(month) - 8
end

--- Fixed date for Roman name `r_date`.
-- @tparam table r_date Roman date {year, month, event, count, leap}.
-- @treturn number Fixed date.
function M.fixed_from_roman(r_date)
  local leap  = roman_leap(r_date)
  local count = roman_count(r_date)
  local event = roman_event(r_date)
  local month = roman_month(r_date)
  local year  = roman_year(r_date)
  local base
  if event == KALENDS then
    base = fixed_from_julian(julian_date(year, month, 1))
  elseif event == NONES then
    base = fixed_from_julian(
      julian_date(year, month, nones_of_month(month))
    )
  else  -- IDES
    base = fixed_from_julian(
      julian_date(year, month, ides_of_month(month))
    )
  end
  local leap_offset
  if julian_leap_year(year)
    and month == MARCH
    and event == KALENDS
    and count >= 6 and count <= 16
  then
    leap_offset = 0  -- After Ides until leap day.
  else
    leap_offset = 1
  end
  return base - count + leap_offset + (leap and 1 or 0)  -- leap is true or false
end
local fixed_from_roman = M.fixed_from_roman

--- Roman name for fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table Roman date {year, month, event, count, leap}.
function M.roman_from_fixed(date)
  local j_date      = julian_from_fixed(date)
  local month       = basic.standard_month(j_date)
  local day         = basic.standard_day(j_date)
  local year        = basic.standard_year(j_date)
  local month_prime = basic.amod(month + 1, 12)
  local year_prime
  if month_prime ~= 1 then
    year_prime = year
  elseif year ~= -1 then
    year_prime = year + 1
  else
    year_prime = 1
  end
  local kalends1 = fixed_from_roman(
    roman_date(year_prime, month_prime, KALENDS, 1, false)
  )
  if day == 1 then
    return roman_date(year, month, KALENDS, 1, false)
  elseif day <= nones_of_month(month) then
    return roman_date(year, month, NONES,
      1 + nones_of_month(month) - day, false)
  elseif day <= ides_of_month(month) then
    return roman_date(year, month, IDES,
      1 + ides_of_month(month) - day, false)
  elseif month ~= FEBRUARY or not julian_leap_year(year) then
    -- After the Ides, in a month that is not February of a leap year.
    return roman_date(year_prime, month_prime, KALENDS,
      1 + kalends1 - date, false)
  elseif day < 25 then
    -- February of a leap year, before leap day.
    return roman_date(year, MARCH, KALENDS, 30 - day, false)
  else
    -- February of a leap year, on or after leap day.
    return roman_date(year, MARCH, KALENDS,
      31 - day, day == 25 and true or false)
  end
end


-- === AUC (Ab Urbe Condita) ===

-- Julian year of the founding of Rome.
local YEAR_ROME_FOUNDED = bce(753)

--- AUC year equivalent to Julian year `j_year`.
-- @tparam number j_year Julian year.
-- @treturn number AUC year.
function M.auc_year_from_julian(j_year)
  if j_year >= YEAR_ROME_FOUNDED and j_year <= -1 then
    return j_year - YEAR_ROME_FOUNDED + 1
  else
    return j_year - YEAR_ROME_FOUNDED
  end
end

--- Julian year equivalent to AUC year `year`.
-- @tparam number year AUC year.
-- @treturn number Julian year.
function M.julian_year_from_auc(year)
  if year >= 1 and year <= -YEAR_ROME_FOUNDED then
    return year + YEAR_ROME_FOUNDED - 1
  else
    return year + YEAR_ROME_FOUNDED
  end
end


-- === Olympiads ===

-- Julian year at which Olympiads start.
local OLYMPIAD_START = bce(776)

local function olympiad(cycle, year)  return {cycle, year} end
local function olympiad_cycle(o_date) return o_date[1] end
local function olympiad_year(o_date)  return o_date[2] end

--- Olympiad {cycle, year} corresponding to Julian year `j_year`.
-- @tparam number j_year Julian year.
-- @treturn table {cycle, year}
function M.olympiad_from_julian_year(j_year)
  local years = j_year - OLYMPIAD_START - (j_year >= 0 and 1 or 0)
  return olympiad(
    1 + basic.quotient(years, 4),
    1 + years % 4
  )
end

--- Julian year corresponding to Olympiad `o_date`.
-- @tparam table o_date Olympiad {cycle, year}.
-- @treturn number Julian year.
function M.julian_year_from_olympiad(o_date)
  local cycle = olympiad_cycle(o_date)
  local year  = olympiad_year(o_date)
  local years = OLYMPIAD_START + 4 * (cycle - 1) + year - 1
  if years < 0 then
    return years
  else
    return years + 1
  end
end


-- === Julian date in Gregorian year ===

--- Fixed dates of Julian month `j_month`, day `j_day` that fall in Gregorian year `g_year`.
-- @tparam number j_month Julian month (1–12).
-- @tparam number j_day Julian day.
-- @tparam number g_year Gregorian year.
-- @treturn table List of fixed dates (0, 1, or 2 entries).
function M.julian_in_gregorian(j_month, j_day, g_year)
  local jan1 = gregorian.gregorian_new_year(g_year)
  local y    = basic.standard_year(julian_from_fixed(jan1))
  local y_prime = y == -1 and 1 or y + 1
  local date0 = fixed_from_julian(julian_date(y,       j_month, j_day))
  local date1 = fixed_from_julian(julian_date(y_prime, j_month, j_day))
  return basic.list_range(
    {date0, date1},
    gregorian.gregorian_year_range(g_year)
  )
end
local julian_in_gregorian = M.julian_in_gregorian


-- === Seasons (astronomical, requires calendrica-astro.lua) ===

-- Moments of a season in a Gregorian year given cycle length and start.
local function cycle_in_gregorian(season, g_year, cap_L, start)
  local year  = gregorian.gregorian_year_range(g_year)
  local pos   = (season / 360) * cap_L
  local cap_delta = pos - (start % cap_L)
  return basic.positions_in_range(pos, cap_L, cap_delta, year)
end

-- Moments of Julian season in a Gregorian year.
local function julian_season_in_gregorian(season, g_year)
  local cap_Y  = 365 + astro.hr(6)
  local offset = (season / 360) * cap_Y
  return cycle_in_gregorian(
    season, g_year, cap_Y,
    fixed_from_julian(julian_date(bce(1), MARCH, 23)) + offset
  )
end


-- === Eastern Orthodox Christmas ===

--- Fixed dates of Eastern Orthodox Christmas (Julian Dec 25) in Gregorian year `g_year`.
-- Returns a list of 0 or 1 dates.
-- @tparam number g_year Gregorian year.
-- @treturn table List of fixed dates.
function M.eastern_orthodox_christmas(g_year)
  return julian_in_gregorian(DECEMBER, 25, g_year)
end



-- === Exports ===

--- Moments of Julian solar season in Gregorian year `g_year`.
-- Uses the Julian year length (365.25 days) rather than the tropical year.
-- `season` is a solar longitude (0=spring, 90=summer, 180=autumn, 270=winter).
-- @function julian_season_in_gregorian
-- @tparam number season  Solar longitude of season (degrees).
-- @tparam number g_year  Gregorian year.
-- @treturn {number,...} Moments.
M.julian_season_in_gregorian = julian_season_in_gregorian

--- January month number (1).
M.JANUARY                    = JANUARY
--- February month number (2).
M.FEBRUARY                   = FEBRUARY
--- March month number (3).
M.MARCH                      = MARCH
--- April month number (4).
M.APRIL                      = APRIL
--- May month number (5).
M.MAY                        = MAY
--- June month number (6).
M.JUNE                       = JUNE
--- July month number (7).
M.JULY                       = JULY
--- August month number (8).
M.AUGUST                     = AUGUST
--- September month number (9).
M.SEPTEMBER                  = SEPTEMBER
--- October month number (10).
M.OCTOBER                    = OCTOBER
--- November month number (11).
M.NOVEMBER                   = NOVEMBER
--- December month number (12).
M.DECEMBER                   = DECEMBER
--- Roman calendar event class: Kalends.
M.KALENDS                    = KALENDS
--- Roman calendar event class: Nones.
M.NONES                      = NONES
--- Roman calendar event class: Ides.
M.IDES                       = IDES

return M
