--- Islamic calendar conversions and holidays.
-- 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-islamic
-- @release 0.1 2026-07-19

local M = {}

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


-- === Epochs ===

-- Friday epoch: CE 622 July 16 (Julian) — used by arithmetic (civil) calendar.
local ISLAMIC_EPOCH = julian.fixed_from_julian(
  julian.julian_date(julian.ce(622), julian.JULY, 16)
)

-- Thursday epoch: CE 622 July 15 (Julian) — used by TBLA variant.
local ISLAMIC_EPOCH_TBLA = julian.fixed_from_julian(
  julian.julian_date(julian.ce(622), julian.JULY, 15)
)


-- === Date constructor ===

--- Construct an Islamic date {year, month, day}.
-- @tparam number year  Islamic year.
-- @tparam number month Islamic month.
-- @tparam number day   Islamic day.
-- @treturn table {year, month, day}
function M.islamic_date(year, month, day)
  return {year, month, day}
end
local islamic_date = M.islamic_date


-- === Leap year ===

-- True if i_year is an Islamic leap year.
local function islamic_leap_year(i_year)
  return (14 + 11 * i_year) % 30 < 11
end


-- === Conversion ===

--- Fixed date equivalent to Islamic date `i_date`.
-- @tparam table i_date Islamic date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_islamic(i_date)
  local month = basic.standard_month(i_date)
  local day   = basic.standard_day(i_date)
  local year  = basic.standard_year(i_date)
  return (ISLAMIC_EPOCH - 1)       -- Days before start of calendar.
    + (year - 1) * 354               -- Ordinary days since epoch.
    + basic.quotient(3 + 11 * year, 30) -- Leap days since epoch.
    + 29 * (month - 1)               -- Days in prior months this year.
    + basic.quotient(month, 2)
    + day                            -- Days so far this month.
end
local fixed_from_islamic = M.fixed_from_islamic

--- Islamic date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.islamic_from_fixed(date)
  local year  = basic.quotient(
    30 * (date - ISLAMIC_EPOCH) + 10646, 10631
  )
  local prior_days = date - fixed_from_islamic(islamic_date(year, 1, 1))
  local month = basic.quotient(11 * prior_days + 330, 325)
  local day   = 1 + date
    - fixed_from_islamic(islamic_date(year, month, 1))
  return islamic_date(year, month, day)
end
local islamic_from_fixed = M.islamic_from_fixed


-- === TBLA variant (Thursday epoch) ===

--- Fixed date equivalent to Islamic TBLA date `i_date`.
-- Identical to the civil calendar but uses the Thursday epoch (one day earlier).
-- @tparam table i_date Islamic date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_islamic_tbla(i_date)
  local month = basic.standard_month(i_date)
  local day   = basic.standard_day(i_date)
  local year  = basic.standard_year(i_date)
  return (ISLAMIC_EPOCH_TBLA - 1)
    + (year - 1) * 354
    + basic.quotient(3 + 11 * year, 30)
    + 29 * (month - 1)
    + basic.quotient(month, 2)
    + day
end
local fixed_from_islamic_tbla = M.fixed_from_islamic_tbla

--- Islamic TBLA date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.islamic_tbla_from_fixed(date)
  local year  = basic.quotient(
    30 * (date - ISLAMIC_EPOCH_TBLA) + 10646, 10631
  )
  local prior_days = date - fixed_from_islamic_tbla(islamic_date(year, 1, 1))
  local month = basic.quotient(11 * prior_days + 330, 325)
  local day   = 1 + date
    - fixed_from_islamic_tbla(islamic_date(year, month, 1))
  return islamic_date(year, month, day)
end


-- === Umm al-Qura variant ===

-- Precomputed month lengths for AH 1300-1600 (bit-packed: 1=30 days, 0=29 days).
-- Each entry is a 12-bit value, one bit per month (MSB = month 1).
-- Source: ICU UMALQURA_MONTHLENGTH table.
local UMALQURA_YEAR_START = 1300
local UMALQURA_YEAR_END   = 1600
local UMALQURA_DATA = {
  0x0AAA,0x0D54,0x0EC9,0x06D4,0x06EA,0x036C,0x0AAD,0x0555,0x06A9,0x0792,
  0x0BA9,0x05D4,0x0ADA,0x055C,0x0D2D,0x0695,0x074A,0x0B54,0x0B6A,0x05AD,
  0x04AE,0x0A4F,0x0517,0x068B,0x06A5,0x0AD5,0x02D6,0x095B,0x049D,0x0A4D,
  0x0D26,0x0D95,0x05AC,0x09B6,0x02BA,0x0A5B,0x052B,0x0A95,0x06CA,0x0AE9,
  0x02F4,0x0976,0x02B6,0x0956,0x0ACA,0x0BA4,0x0BD2,0x05D9,0x02DC,0x096D,
  0x054D,0x0AA5,0x0B52,0x0BA5,0x05B4,0x09B6,0x0557,0x0297,0x054B,0x06A3,
  0x0752,0x0B65,0x056A,0x0AAB,0x052B,0x0C95,0x0D4A,0x0DA5,0x05CA,0x0AD6,
  0x0957,0x04AB,0x094B,0x0AA5,0x0B52,0x0B6A,0x0575,0x0276,0x08B7,0x045B,
  0x0555,0x05A9,0x05B4,0x09DA,0x04DD,0x026E,0x0936,0x0AAA,0x0D54,0x0DB2,
  0x05D5,0x02DA,0x095B,0x04AB,0x0A55,0x0B49,0x0B64,0x0B71,0x05B4,0x0AB5,
  0x0A55,0x0D25,0x0E92,0x0EC9,0x06D4,0x0AE9,0x096B,0x04AB,0x0A93,0x0D49,
  0x0DA4,0x0DB2,0x0AB9,0x04BA,0x0A5B,0x052B,0x0A95,0x0B2A,0x0B55,0x055C,
  0x04BD,0x023D,0x091D,0x0A95,0x0B4A,0x0B5A,0x056D,0x02B6,0x093B,0x049B,
  0x0655,0x06A9,0x0754,0x0B6A,0x056C,0x0AAD,0x0555,0x0B29,0x0B92,0x0BA9,
  0x05D4,0x0ADA,0x055A,0x0AAB,0x0595,0x0749,0x0764,0x0BAA,0x05B5,0x02B6,
  0x0A56,0x0E4D,0x0B25,0x0B52,0x0B6A,0x05AD,0x02AE,0x092F,0x0497,0x064B,
  0x06A5,0x06AC,0x0AD6,0x055D,0x049D,0x0A4D,0x0D16,0x0D95,0x05AA,0x05B5,
  0x02DA,0x095B,0x04AD,0x0595,0x06CA,0x06E4,0x0AEA,0x04F5,0x02B6,0x0956,
  0x0AAA,0x0B54,0x0BD2,0x05D9,0x02EA,0x096D,0x04AD,0x0A95,0x0B4A,0x0BA5,
  0x05B2,0x09B5,0x04D6,0x0A97,0x0547,0x0693,0x0749,0x0B55,0x056A,0x0A6B,
  0x052B,0x0A8B,0x0D46,0x0DA3,0x05CA,0x0AD6,0x04DB,0x026B,0x094B,0x0AA5,
  0x0B52,0x0B69,0x0575,0x0176,0x08B7,0x025B,0x052B,0x0565,0x05B4,0x09DA,
  0x04ED,0x016D,0x08B6,0x0AA6,0x0D52,0x0DA9,0x05D4,0x0ADA,0x095B,0x04AB,
  0x0653,0x0729,0x0762,0x0BA9,0x05B2,0x0AB5,0x0555,0x0B25,0x0D92,0x0EC9,
  0x06D2,0x0AE9,0x056B,0x04AB,0x0A55,0x0D29,0x0D54,0x0DAA,0x09B5,0x04BA,
  0x0A3B,0x049B,0x0A4D,0x0AAA,0x0AD5,0x02DA,0x095D,0x045E,0x0A2E,0x0C9A,
  0x0D55,0x06B2,0x06B9,0x04BA,0x0A5D,0x052D,0x0A95,0x0B52,0x0BA8,0x0BB4,
  0x05B9,0x02DA,0x095A,0x0B4A,0x0DA4,0x0ED1,0x06E8,0x0B6A,0x056D,0x0535,
  0x0695,0x0D4A,0x0DA8,0x0DD4,0x06DA,0x055B,0x029D,0x062B,0x0B15,0x0B4A,
  0x0B95,0x05AA,0x0AAE,0x092E,0x0C8F,0x0527,0x0695,0x06AA,0x0AD6,0x055D,0x029D,
}

local function umalqura_month_length(year, month)
  if year < UMALQURA_YEAR_START or year > UMALQURA_YEAR_END then
    return 29 + (month % 2 == 1 and 1 or 0)
      + (month == 12 and islamic_leap_year(year) and 1 or 0)
  end
  local bits = UMALQURA_DATA[year - UMALQURA_YEAR_START + 1]
  local mask = 0x800 >> (month - 1)
  return (bits & mask ~= 0) and 30 or 29
end

--- Fixed date equivalent to Umm al-Qura Islamic date `i_date`.
-- @tparam table i_date Islamic date {year, month, day}.
-- @treturn number Fixed date.
function M.fixed_from_islamic_umalqura(i_date)
  local month = basic.standard_month(i_date)
  local day   = basic.standard_day(i_date)
  local year  = basic.standard_year(i_date)
  -- start from year epoch using civil calendar for years outside table range
  local d = fixed_from_islamic(islamic_date(year, 1, 1))
  -- adjust for umalqura month lengths within the year
  for m = 1, month - 1 do
    d = d + umalqura_month_length(year, m)
  end
  return d + day - 1
end
local fixed_from_islamic_umalqura = M.fixed_from_islamic_umalqura

--- Umm al-Qura Islamic date {year, month, day} corresponding to fixed `date`.
-- @tparam number date Fixed date.
-- @treturn table {year, month, day}
function M.islamic_umalqura_from_fixed(date)
  -- estimate year from civil calendar
  local year = basic.standard_year(islamic_from_fixed(date))
  -- adjust year if needed
  while fixed_from_islamic_umalqura(islamic_date(year + 1, 1, 1)) <= date do
    year = year + 1
  end
  while fixed_from_islamic_umalqura(islamic_date(year, 1, 1)) > date do
    year = year - 1
  end
  local month = 1
  while month < 12 and
    fixed_from_islamic_umalqura(islamic_date(year, month + 1, 1)) <= date do
    month = month + 1
  end
  local day = 1 + date - fixed_from_islamic_umalqura(islamic_date(year, month, 1))
  return islamic_date(year, month, day)
end


-- === Islamic date in Gregorian year ===

-- Fixed dates of Islamic i_month/i_day that fall in Gregorian year g_year.
local function islamic_in_gregorian(i_month, i_day, g_year)
  local jan1  = gregorian.gregorian_new_year(g_year)
  local y     = basic.standard_year(islamic_from_fixed(jan1))
  local date0 = fixed_from_islamic(islamic_date(y,     i_month, i_day))
  local date1 = fixed_from_islamic(islamic_date(y + 1, i_month, i_day))
  local date2 = fixed_from_islamic(islamic_date(y + 2, i_month, i_day))
  return basic.list_range(
    {date0, date1, date2},
    gregorian.gregorian_year_range(g_year)
  )
end


-- === Islamic holidays ===

--- Fixed dates of Ramadan start (month 9, day 1) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.ramadan(g_year)
  return islamic_in_gregorian(9, 1, g_year)
end

--- Fixed dates of Eid al-Fitr (month 10, day 1) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.eid_al_fitr(g_year)
  return islamic_in_gregorian(10, 1, g_year)
end

--- Fixed dates of Eid al-Adha (month 12, day 10) in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.eid_al_adha(g_year)
  return islamic_in_gregorian(12, 10, g_year)
end

--- Fixed dates of Islamic New Year (Muharram 1) occurring in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn {number,...} Fixed dates.
function M.islamic_new_year(g_year)
  return islamic_in_gregorian(1, 1, g_year)
end

--- Fixed dates of Mawlid an-Nabi occurring in Gregorian year `g_year`.
-- @tparam number g_year Gregorian year.
-- @treturn table List of fixed dates.
function M.mawlid(g_year)
  return islamic_in_gregorian(3, 12, g_year)
end



-- === Exports ===

--- Fixed dates of Islamic month/day `i_month`/`i_day` falling in Gregorian year `g_year`.
-- @function islamic_in_gregorian
-- @tparam number i_month Islamic month.
-- @tparam number i_day   Islamic day.
-- @tparam number g_year  Gregorian year.
-- @treturn {number,...} Fixed dates.
M.islamic_in_gregorian = islamic_in_gregorian

--- True if Islamic year `i_year` is a leap year (355 days).
-- @function islamic_leap_year
-- @tparam number i_year Islamic year.
-- @treturn boolean
M.islamic_leap_year = islamic_leap_year

--- Fixed date of start of the Islamic calendar (July 16, 622 CE Julian).
M.ISLAMIC_EPOCH      = ISLAMIC_EPOCH

--- Fixed date of start of the Islamic TBLA calendar (July 15, 622 CE Julian).
M.ISLAMIC_EPOCH_TBLA = ISLAMIC_EPOCH_TBLA

return M
