--- French Revolutionary 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-french-revolutionary
-- @release 0.1 2026-07-19

local M = {}

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


-- === Epoch and location ===

-- French Revolutionary epoch: September 22, 1792 (Gregorian).
local FRENCH_EPOCH = gregorian.fixed_from_gregorian(
  gregorian.gregorian_date(1792, gregorian.SEPTEMBER, 22)
)

-- Location of Paris Observatory.
local PARIS = astro.location(
  astro.angle(48, 50, 11),
  astro.angle(2, 20, 15),
  astro.mt(27),
  astro.hr(1)
)


-- === Date constructor ===

-- French Revolutionary date constructor.
local function french_date(year, month, day)
  return {year, month, day}
end


-- === Astronomical French Revolutionary calendar ===

-- Universal time of true midnight at end of date in Paris.
local function midnight_in_paris(date)
  return astro.midnight(date + 1, PARIS)
end

-- French New Year on or before date (autumnal equinox in Paris).
local function french_new_year_on_or_before(date)
  local approx = astro.estimate_prior_solar_longitude(
    astro.AUTUMN, midnight_in_paris(date)
  )
  return basic.next(
    math.floor(approx) - 1,
    function(day)
      return astro.AUTUMN <= astro.solar_longitude(midnight_in_paris(day))
    end
  )
end

--- Fixed date of astronomical French Revolutionary date `f_date`.
-- @tparam table f_date French date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_french(f_date)
  local month = basic.standard_month(f_date)
  local day   = basic.standard_day(f_date)
  local year  = basic.standard_year(f_date)
  local new_year = french_new_year_on_or_before(
    math.floor(
      FRENCH_EPOCH + 180     -- Spring after epoch.
      + astro.MEAN_TROPICAL_YEAR * (year - 1)
    )
  )
  return new_year - 1        -- Days in prior years.
    + 30 * (month - 1)       -- Days in prior months.
    + day                    -- Days this month.
end
local fixed_from_french = M.fixed_from_french

--- Astronomical French Revolutionary date {year, month, day} for fixed `date`.
-- Uses `round_half_to_even` to match Lisp's `round`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.french_from_fixed(date)
  local new_year = french_new_year_on_or_before(date)
  local year  = 1 + basic.round_half_to_even(
    (new_year - FRENCH_EPOCH) / astro.MEAN_TROPICAL_YEAR
  )
  local month = 1 + basic.quotient(date - new_year, 30)
  local day   = 1 + (date - new_year) % 30
  return french_date(year, month, day)
end

--- True if `f_year` is an astronomical French Revolutionary leap year.
-- @tparam number f_year French Revolutionary year.
-- @treturn boolean
function M.french_leap_year(f_year)
  return fixed_from_french(french_date(f_year + 1, 1, 1))
       - fixed_from_french(french_date(f_year, 1, 1))
       > 365
end


-- === Arithmetic French Revolutionary calendar ===

--- True if `f_year` is an arithmetic French Revolutionary leap year.
-- @tparam number f_year French Revolutionary year.
-- @treturn boolean
function M.arithmetic_french_leap_year(f_year)
  return f_year % 4 == 0
    and (f_year % 400 == 0 or f_year % 100 ~= 0)
    and f_year % 4000 ~= 0
end

--- Fixed date of Arithmetic French Revolutionary date `f_date`.
-- @tparam table f_date Arithmetic French date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_arithmetic_french(f_date)
  local month = basic.standard_month(f_date)
  local day   = basic.standard_day(f_date)
  local year  = basic.standard_year(f_date)
  return FRENCH_EPOCH - 1               -- Days before start of calendar.
    + 365 * (year - 1)                   -- Ordinary days in prior years.
    + basic.quotient(year - 1, 4)        -- Leap days in prior years.
    - basic.quotient(year - 1, 100)      -- Minus century years.
    + basic.quotient(year - 1, 400)      -- Plus 400-year cycles.
    - basic.quotient(year - 1, 4000)     -- Minus 4000-year cycles.
    + 30 * (month - 1)                   -- Days in prior months this year.
    + day                                -- Days this month.
end
local fixed_from_arithmetic_french = M.fixed_from_arithmetic_french

--- Arithmetic French Revolutionary date {year, month, day} of fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.arithmetic_french_from_fixed(date)
  -- Approximate year (may be off by 1).
  local approx = 1 + basic.quotient(
    date - FRENCH_EPOCH + 2,
    1460969 / 4000
  )
  local year
  if date < fixed_from_arithmetic_french(french_date(approx, 1, 1)) then
    year = approx - 1
  else
    year = approx
  end
  local month = 1 + basic.quotient(
    date - fixed_from_arithmetic_french(french_date(year, 1, 1)),
    30
  )
  local day = 1 + date
    - fixed_from_arithmetic_french(french_date(year, month, 1))
  return french_date(year, month, day)
end


return M
