--- Balinese Pawukon 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-balinese
-- @release 0.1 2026-07-19

local M = {}

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


-- === Epoch ===

-- Fixed date of start of a Balinese Pawukon cycle.
local BALI_EPOCH = basic.fixed_from_jd(146)


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

-- Balinese Pawukon date constructor.
local function balinese_date(b1, b2, b3, b4, b5, b6, b7, b8, b9, b0)
  return {b1, b2, b3, b4, b5, b6, b7, b8, b9, b0}
end

-- Balinese Pawukon cycle accessors.
local function bali_luang(b)     return b[1]  end -- luacheck: ignore
local function bali_dwiwara(b)   return b[2]  end -- luacheck: ignore
local function bali_triwara(b)   return b[3]  end -- luacheck: ignore
local function bali_caturwara(b) return b[4]  end -- luacheck: ignore
local function bali_pancawara(b) return b[5]  end
local function bali_sadwara(b)   return b[6]  end
local function bali_saptawara(b) return b[7]  end
local function bali_asatawara(b) return b[8]  end -- luacheck: ignore
local function bali_sangawara(b) return b[9]  end -- luacheck: ignore
local function bali_dasawara(b)  return b[10] end -- luacheck: ignore


-- === Day position in Pawukon cycle ===

-- Position of date in 210-day Pawukon cycle.
local function bali_day_from_fixed(date)
  return (date - BALI_EPOCH) % 210
end


-- === Individual cycle positions ===

-- Forward declarations for mutual recursion.
local bali_pancawara_from_fixed
local bali_saptawara_from_fixed

-- Position of date in 10-day Balinese cycle.
local function bali_dasawara_from_fixed(date)
  local i = bali_pancawara_from_fixed(date) - 1
  local j = bali_saptawara_from_fixed(date) - 1
  local pancawara_offset = ({5, 9, 7, 4, 8})[i + 1]
  local saptawara_offset = ({5, 4, 3, 7, 8, 6, 9})[j + 1]
  return (1 + pancawara_offset + saptawara_offset) % 10
end

-- True if dasawara position is even.
local function bali_luang_from_fixed(date)
  return bali_dasawara_from_fixed(date) % 2 == 0
end

-- Position of date in 2-day Balinese cycle.
local function bali_dwiwara_from_fixed(date)
  return basic.amod(bali_dasawara_from_fixed(date), 2)
end

-- Position of date in 3-day Balinese cycle.
local function bali_triwara_from_fixed(date)
  return 1 + (bali_day_from_fixed(date) % 3)
end

-- Position of date in 5-day Balinese cycle.
bali_pancawara_from_fixed = function(date)
  return basic.amod(bali_day_from_fixed(date) + 2, 5)
end

-- Position of date in 6-day Balinese cycle.
local function bali_sadwara_from_fixed(date)
  return 1 + (bali_day_from_fixed(date) % 6)
end

-- Position of date in Balinese week (7-day cycle).
bali_saptawara_from_fixed = function(date)
  return 1 + (bali_day_from_fixed(date) % 7)
end

-- Position of date in 8-day Balinese cycle.
local function bali_asatawara_from_fixed(date)
  local day = bali_day_from_fixed(date)
  return 1 + (math.max(6, 4 + (day - 70) % 210) % 8)
end

-- Position of date in 9-day Balinese cycle.
local function bali_sangawara_from_fixed(date)
  return 1 + (math.max(0, bali_day_from_fixed(date) - 3) % 9)
end

-- Position of date in 4-day Balinese cycle.
local function bali_caturwara_from_fixed(date)
  return basic.amod(bali_asatawara_from_fixed(date), 4)
end


-- === Full Pawukon date ===

--- Positions of `date` in ten cycles of the Balinese Pawukon calendar.
-- @tparam number date Fixed date.
-- @treturn table Balinese date {luang, dwiwara, triwara, caturwara, pancawara,
--   sadwara, saptawara, asatawara, sangawara, dasawara}
function M.bali_pawukon_from_fixed(date)
  return balinese_date(
    bali_luang_from_fixed(date),
    bali_dwiwara_from_fixed(date),
    bali_triwara_from_fixed(date),
    bali_caturwara_from_fixed(date),
    bali_pancawara_from_fixed(date),
    bali_sadwara_from_fixed(date),
    bali_saptawara_from_fixed(date),
    bali_asatawara_from_fixed(date),
    bali_sangawara_from_fixed(date),
    bali_dasawara_from_fixed(date)
  )
end

-- Week number of date in Balinese Pawukon cycle.
local function bali_week_from_fixed(date)
  return 1 + basic.quotient(bali_day_from_fixed(date), 7)
end


-- === Inverse: find date on or before ===

-- Last fixed date on or before date with Pawukon b_date.
local function bali_on_or_before(b_date, date)
  local pancawara  = bali_pancawara(b_date)
  local sadwara    = bali_sadwara(b_date)
  local saptawara  = bali_saptawara(b_date)
  local a5  = pancawara - 1
  local a6  = sadwara - 1
  local b7  = saptawara - 1
  local b35 = (a5 + 14 + 15 * (b7 - a5)) % 35
  local days = a6 + 36 * (b35 - a6)
  local cap_delta = bali_day_from_fixed(basic.rd(0))
  return date - ((date + cap_delta - days) % 210)
end


-- === Recurring dates ===

--- Occurrences of Kajeng Keliwon in Gregorian year `g_year`.
-- Kajeng Keliwon falls every 15 days when the 3-day and 5-day weeks coincide.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.kajeng_keliwon(g_year)
  local year      = gregorian.gregorian_year_range(g_year)
  local cap_delta = bali_day_from_fixed(basic.rd(0))
  return basic.positions_in_range(8, 15, cap_delta, year)
end

--- Occurrences of Tumpek in Gregorian year `g_year`.
-- Tumpek falls every 35 days (intersection of 5-day and 7-day weeks).
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.tumpek(g_year)
  local year      = gregorian.gregorian_year_range(g_year)
  local cap_delta = bali_day_from_fixed(basic.rd(0))
  return basic.positions_in_range(13, 35, cap_delta, year)
end

--- Week number (1..30) of `date` in the 210-day Balinese Pawukon cycle.
-- @function bali_week_from_fixed
-- @tparam number date Fixed date.
-- @treturn number Week number.
M.bali_week_from_fixed = bali_week_from_fixed

--- Latest fixed date on or before `date` whose Pawukon equals `b_date`.
-- @function bali_on_or_before
-- @tparam table  b_date Balinese Pawukon date.
-- @tparam number date   Fixed date.
-- @treturn number Fixed date.
M.bali_on_or_before = bali_on_or_before


return M
