--- Tibetan calendar conversions.
-- 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-tibetan
-- @release 0.1 2026-07-19

local M = {}


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


-- === Epoch ===

-- Fixed date of start of the Tibetan calendar (December 7, -127 Gregorian).
local TIBETAN_EPOCH = gregorian.fixed_from_gregorian(
  gregorian.gregorian_date(-127, gregorian.DECEMBER, 7)
)


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

-- Tibetan date constructor.
local function tibetan_date(year, month, leap_month, day, leap_day)
  return {year, month, leap_month, day, leap_day}
end

-- Tibetan date accessors.
local function tibetan_year(date)        return date[1] end
local function tibetan_month(date)       return date[2] end
local function tibetan_leap_month(date)  return date[3] end
local function tibetan_day(date)         return date[4] end
local function tibetan_leap_day(date)    return date[5] end


-- === Equation tables ===

-- Interpolated tabular sine of solar anomaly alpha (range 0..12).
local function tibetan_sun_equation(alpha)
  if alpha > 6 then
    return -tibetan_sun_equation(alpha - 6)
  elseif alpha > 3 then
    return tibetan_sun_equation(6 - alpha)
  elseif alpha % 1 == 0 then
    local table = {astro.mins(0), astro.mins(6),
                   astro.mins(10), astro.mins(11)}
    return table[alpha + 1]
  else
    return (alpha % 1) * tibetan_sun_equation(math.ceil(alpha))
         + ((-alpha) % 1) * tibetan_sun_equation(math.floor(alpha))
  end
end

-- Interpolated tabular sine of lunar anomaly alpha (range 0..14).
local function tibetan_moon_equation(alpha)
  if alpha > 14 then
    return -tibetan_moon_equation(alpha - 14)
  elseif alpha > 7 then
    return tibetan_moon_equation(14 - alpha)
  elseif alpha % 1 == 0 then
    local table = {
      astro.mins(0),  astro.mins(5),  astro.mins(10), astro.mins(15),
      astro.mins(19), astro.mins(22), astro.mins(24), astro.mins(25),
    }
    return table[alpha + 1]
  else
    return (alpha % 1) * tibetan_moon_equation(math.ceil(alpha))
         + ((-alpha) % 1) * tibetan_moon_equation(math.floor(alpha))
  end
end


-- === Conversion ===

--- Fixed date corresponding to Tibetan lunar date `t_date`.
-- @tparam table t_date Tibetan date {year, month, leap_month, day, leap_day}.
-- @treturn number Fixed date.
function M.fixed_from_tibetan(t_date)
  local year       = tibetan_year(t_date)
  local month      = tibetan_month(t_date)
  local leap_month = tibetan_leap_month(t_date)
  local day        = tibetan_day(t_date)
  local leap_day   = tibetan_leap_day(t_date)
  local months = math.floor(
    804/65 * (year - 1) + 67/65 * month
    + (leap_month and -1 or 0)
    + 64/65
  )
  local days = 30 * months + day
  local mean = days * 11135/11312 - 30
    + (leap_day and 0 or -1)
    + 1071/1616
  local solar_anomaly = (days * 13/4824 + 2117/4824) % 1
  local lunar_anomaly = (days * 3781/105840 + 2837/15120) % 1
  local sun  = -tibetan_sun_equation(12 * solar_anomaly)
  local moon =  tibetan_moon_equation(28 * lunar_anomaly)
  return math.floor(TIBETAN_EPOCH + mean + sun + moon)
end
local fixed_from_tibetan = M.fixed_from_tibetan

--- Tibetan lunar date {year,month,leap_month,day,leap_day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, leap_month, day, leap_day}
function M.tibetan_from_fixed(date)
  local cap_Y = 365 + 4975/18382
  local years = math.ceil((date - TIBETAN_EPOCH) / cap_Y)
  local year0 = basic.final(
    years,
    function(y)
      return date >= fixed_from_tibetan(tibetan_date(y, 1, false, 1, false))
    end
  )
  local month0 = basic.final(
    1,
    function(m)
      return date >= fixed_from_tibetan(
        tibetan_date(year0, m, false, 1, false)
      )
    end
  )
  local est = date - fixed_from_tibetan(
    tibetan_date(year0, month0, false, 1, false)
  )
  local day0 = basic.final(
    est - 2,
    function(d)
      return date >= fixed_from_tibetan(
        tibetan_date(year0, month0, false, d, false)
      )
    end
  )
  local leap_month = day0 > 30
  local day        = basic.amod(day0, 30)
  local month_raw
  if day > day0 then
    month_raw = month0 - 1
  elseif leap_month then
    month_raw = month0 + 1
  else
    month_raw = month0
  end
  local month = basic.amod(month_raw, 12)
  local year
  if day > day0 and month0 == 1 then
    year = year0 - 1
  elseif leap_month and month0 == 12 then
    year = year0 + 1
  else
    year = year0
  end
  local leap_day = date == fixed_from_tibetan(
    tibetan_date(year, month, leap_month, day, true)
  )
  return tibetan_date(year, month, leap_month, day, leap_day)
end
local tibetan_from_fixed = M.tibetan_from_fixed


-- === Leap month and day predicates ===

-- True if t_month is a leap month in Tibetan year t_year.
local function tibetan_leap_month_p(t_year, t_month)
  return t_month == tibetan_month(
    tibetan_from_fixed(
      fixed_from_tibetan(
        tibetan_date(t_year, t_month, true, 2, false)
      )
    )
  )
end

-- True if t_day is a leap day in Tibetan month t_month and year t_year.
local function tibetan_leap_day_p(t_year, t_month, t_day)
  return t_day == tibetan_day(
    tibetan_from_fixed(
      fixed_from_tibetan(
        tibetan_date(t_year, t_month, false, t_day, true)
      )
    )
  )
  or t_day == tibetan_day(
    tibetan_from_fixed(
      fixed_from_tibetan(
        tibetan_date(
          t_year, t_month,
          tibetan_leap_month_p(t_year, t_month),
          t_day, true
        )
      )
    )
  )
end


-- === Tibetan New Year ===

-- Fixed date of Tibetan New Year (Losar) in Tibetan year t_year.
local function losar(t_year)
  local t_leap = tibetan_leap_month_p(t_year, 1)
  return fixed_from_tibetan(tibetan_date(t_year, 1, t_leap, 1, false))
end

-- List of fixed dates of Tibetan New Year in Gregorian year g_year.
--- Fixed dates of Losar (Tibetan New Year) falling in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.tibetan_new_year(g_year)
  local dec31  = gregorian.gregorian_year_end(g_year)
  local t_year = tibetan_year(tibetan_from_fixed(dec31))
  return basic.list_range(
    {losar(t_year - 1), losar(t_year)},
    gregorian.gregorian_year_range(g_year)
  )
end

--- True if month `t_month` is a leap month in Tibetan year `t_year`.
-- @function tibetan_leap_month_p
-- @tparam number t_year  Tibetan year.
-- @tparam number t_month Tibetan month.
-- @treturn boolean
M.tibetan_leap_month_p = tibetan_leap_month_p

--- True if day `t_day` is a leap day in Tibetan month `t_month` of year `t_year`.
-- @function tibetan_leap_day_p
-- @tparam number t_year  Tibetan year.
-- @tparam number t_month Tibetan month.
-- @tparam number t_day   Tibetan day.
-- @treturn boolean
M.tibetan_leap_day_p = tibetan_leap_day_p


return M
