--[[--------------- luaHijri---------------------
by hubaishan
v0.5
locate at https://github.com/hubaishan/luaHijri

]]
--[[ ------------ C O N F I G U R A T I O N S ----------------------]]
--[[ As
local adjust_data = { [1443] = 
	[5] = {2021,12,6},
	[7] = 1 }
	this means ad just start of month 5 of 1443 to 2021-12-06, and add one day to month 7 of 1443  

]]
--local adjust_data = {} --adjust data in separate module
-- Gregorian Calender epoch used only for wertern calendar default value are in Italy some countries may differ
local gregorian_epoch = { jd = 2299161, y = 1582 , m = 10, d = 15}
-- sunday index can be 0 or 1
local wday_sunday = 0
-- day of year (1-1) index
local yday_1_1 = 0
-- @var int 1 to use adjusted Um Al-Qura algorithm, 2 to use Um Al-Qura algorithm, 0 use Hijri Tabular Algorithm
local hijri_mode = mw.loadData('Module:Hijri/Configuration').hijri_mode
--[[ ------------ C O N S T A N T S ----------------]]
--  the Um Al-Qura Calendar start year
local umstartyear = 1318
--  the Um Al-Qura Calendar end year
local umendyear = 1500
--  the Um Al-Qura Calendar start Julian day
local umstartjd = 2415140
-- the Um Al-Qura Calendar end  Julian day
local umendjd = 2479960
local UMDATA_COUNT = 2196
--[[ ------------ V A R S ----------------]]
local adjusted_umdata
local raw_umdata
--[[------------------- U T I L T Y   U S E F U L   F U N C T I O N S------------------------]]
local floor = math.floor

local function round(number)
	return floor(number + 0.5)
end

local function in_array( needle, haystack )
	if needle == nil then
		return false;
	end
	for n,v in ipairs( haystack ) do
		if v == needle then
			return n;
		end
	end
	return false;
end

--[[------------------- C A L E N D A R   F U N C T I O N S------------------------]]

--[[--- Data ---]]
local function gregorian2jd(year, month, day)
	-- from postgresql source
	local jd, century
	mw.logObject(year,'year')
	if year < 1 then
		year = year + 1
	end
	
	if month > 2 then
		month = month + 1
		year = year + 4800
	else
		month = month + 13
		year = year + 4799
	end

	century = floor(year / 100)

	return (year * 365 - 32167 + floor(year / 4) - century + floor(century / 4) + floor(7834 * month / 256) + day)
end

-- load Umm al-Qura Calendar Data
-- Modified for MediaWiki
local function get_umdata(mode)
	mode = mode or hijri_mode

	if mode==2 then
		if raw_umdata==nil or #raw_umdata==0 then		
			raw_umdata = mw.loadData('Module:Hijri/umalqura data')	
		end
		return raw_umdata
	elseif mode==1 then
		if adjusted_umdata == nil or #adjusted_umdata==0 then
			adjusted_umdata = mw.loadData('Module:Hijri/adjusted data')
		end
		return adjusted_umdata 
	end
end

--[[--- Hijri ---]]

local function jd2hijri(julianday, mode)
    local mjd, ii, n, y, i
	local j, j1
	local hy, hm, hd, hz
	mode = mode or hijri_mode
	if (mode>0 and (julianday > umstartjd) and (julianday < umendjd)) then
		local umdata = get_umdata(mode)
		i = floor((julianday - 1948438) / 29.53056) - ((umstartyear - 1) * 12)
        mjd = julianday - 2400000
        if i<0 then i = 0 end
        for c = i, UMDATA_COUNT do
            if (umdata[c] > mjd) then
				i = c
                break
            end
		end
		ii = floor((i - 1) / 12) 
		hy = umstartyear + ii
		hm = i - 12 * ii 
		hd = mjd - umdata[i - 1] + 1
		hz = mjd - umdata[12 * ii]
	else
		j = julianday + 7666
		n = floor(j / 10631)
		j = j - (n * 10631)
		j1 = j
		y = floor(j / 354.36667)
		j = j - round(y * 354.36667)
		if j == 0 then
			y=y-1
			j = j1 - round(y * 354.36667)
			hz = j
			hd = j - 325
			hm = 12
		else
			hz = j - 1
			j = j + 29
			hm = floor((24 * j) / 709)
			hd = j -  floor(((709 * hm) / 24))
		end
		hy = (n * 30) + y + 1
		hy = hy - 5520
		if hy <= 0 then
			hy = hy - 1
		end
    end
	hz = hz + yday_1_1
    return hy, hm, hd, hz
end


local function hijri2jd(hy, hm, hd, mode)
	mode = mode or hijri_mode
	local ii, i, n
	local j
	if (mode > 0 and hy >= umstartyear and hy <= umendyear) then
		local umdata = get_umdata(mode)
		ii = hy - umstartyear
		i = hm + 12 * ii
		j = hd + umdata[i - 1] - 1
		j = j + 2400000
	elseif (hy < -5499 or (hy == -5499 and hm < 8) or (hy == -5499 and hm == 8 and hd < 18)) then
		j = 0
	else
		hy = hy < 0 and (hy + 5520) or (hy + 5519)
		n = floor(hy / 30)
		j = (n * 10631) + round((hy - (n * 30)) * 354.36667)
		hm = hm - 1
		j = j + (hm * 29) + floor((hm + 1) / 2) + hd
		j = j- 7666
	end
	return j
end

local function hijri_days_in_month(year, month, mode)
	mode = mode or hijri_mode
	local t = 29
	if mode>0 and year >= umstartyear and year <= umendyear then
		local i = (year-umstartyear)*12 + month -1
		local umdata = get_umdata(mode)
		t = umdata[i + 1] - umdata[i]
	else
		if month == 12 then
			if year < 0 then
				year = year + 5521
			end

			if round((year % 30) * 0.36667) > round(((year - 1) % 30) * 0.36667) then
				t = 30
			end
		else
			t = 29 + (month % 2);
		end
	end
	return t
end

local function hijri_isleap(year, mode)
	mode = mode or hijri_mode
	local l
	if mode>0 and year >= umstartyear and year <= umendyear then
		local umdata = get_umdata(mode)
		local ii = year - umstartyear
		l = ((umdata[12 * (ii + 1)] - umdata[12 * ii]) > 354) and true or false
	else
		if year < 0 then
			year = year + 5521;
		end
		l = (round((year % 30) * 0.36667) > round(((year - 1) % 30) * 0.36667)) and true or false
	end
	return l
end

local function hijri_yday(year,month,day,mode)
    local ii
	mode = mode or hijri_mode
	if (mode>0 and year >= umstartyear and year <= umendyear) then
		local umdata = get_umdata(mode)
		ii = year - umstartyear
		return umdata[(12 * ii) + month -1] - umdata[12 * ii] + day - 1 + yday_1_1
	else
		return select(month, 0,30,59,89,118,148,177,207,236,266,295,325) + day - 1 + yday_1_1 
	end
end

local function hijri_check(year,month,day,mode,may_adjusted)
	if month < 1 or month > 12 then
		return false
	end

	if day < 1 or day > 30 then
		return false
	end

	if not may_adjusted and day > hijri_days_in_month(year,month,mode) then
		return false
	end

	return true
end
--[[--- Unix and wday---]]

local function jd2unix(jd)
	-- 2440588 -- J.D. of 1.1.1970
	if jd == nil then
		return nil
	end
	return ((jd - 2440588) * 24 * 3600)
end

local function unix2jd(unix)
	if unix == nil then
		return nil
	end
	return floor(unix/(24 * 3600)) + 2440588
end

--returns wday number sunday=wday_sunday
local function jd2wday(jd)
	jd = jd + 1
	jd = jd % 7
	if (jd<0) then
		jd = jd + 7
	end
	return jd + wday_sunday
end

--[[--- Gregorian ---]]

local function jd2gregorian(jd)
	-- inspired by postgresql function
	local julian, quad, extra, y
	local year,month,day
	julian = jd + 32044
	
	quad = floor(julian / 146097)
	
	extra = (julian - quad * 146097) * 4 + 3
	julian = julian + 60 + quad * 3 + floor(extra / 146097)
	quad = floor(julian / 1461)
	julian = julian - quad * 1461
	y = floor(julian * 4 / 1461)
	
	if (y ~= 0) then
		julian = ((julian + 305) % 365) + 123
	else
		julian = ((julian + 306) % 366) + 123
	end
	y = y + quad * 4;
	year = y - 4800;
	
	quad = floor(julian * 2141 / 65536)
	day = julian - floor(7834 * quad / 256)
	month = (quad + 10) % 12 + 1

	if (year<1) then
		year = year-1
	end
	return year,month,day
end

local function gre_isleap(year)
	if year<1 then year=year+1 end
	return (year % 4) == 0 and ((year % 100) ~= 0 or (year % 400) == 0)
end

local function gre_yday(year,month,day)
	if gre_isleap(year) then
		return  select(month,0,31,60,91,121,152,182,213,244,274,305,335) + day - 1 + yday_1_1
	else
		return  select(month,0,31,59,90,120,151,181,212,243,273,304,334) + day - 1 + yday_1_1
	end
end

local function gre_days_in_month(year, month)
	if month == 2 and gre_isleap(year) then
		return 29
	else
		return select(month,31,28,31,30,31,30,31,31,30,31,30,31)
	end
end

local function gre_check(year,month,day)
	if month < 1 or month > 12 then
		return false
	end

	if day < 1 or day > gre_days_in_month(year,month) then
		return false
	end
	return true
end
--[[--- Julian ---]]

local function jd2julian(jd)
	local year, month, day
	local temp, dayofYear

	temp = jd * 4 + (32083 * 4 - 1)

	year = floor(temp / 1461)
	dayofYear = floor((temp % 1461) / 4) + 1

	temp = dayofYear * 5 - 3
	month = floor(temp / 153)
	day = floor((temp % 153) / 5) + 1

	if (month < 10) then
		month = month + 3
	else
		year = year + 1
		month = month - 9
	end

	year = year - 4800
	if (year < 1) then
		year = year -1
	end

	return year, month, day
end

local function julian2jd(year,month,day)
	if year<0 then
		year = year + 4801
	else
		year = year + 4800
	end

	if month > 2 then
		month = month -3
	else
		month = month + 9
		year = year - 1
	end

	return (floor((year * 1461) / 4) + floor((month * 153 + 2) / 5) + day - 32083)
end

local function julian_isleap(year)
	if year < 1 then year = year + 1 end
	return (year % 4) == 0
end

local function julian_yday(year,month,day)
	if julian_isleap(year) then
		return  select(month,0,31,60,91,121,152,182,213,244,274,305,335) + day - 1 + yday_1_1
	else
		return  select(month,0,31,59,90,120,151,181,212,243,273,304,334) + day - 1 + yday_1_1
	end
end

local function julian_days_in_month(year, month)
	if month == 2 and julian_isleap(year) then
		return 29
	else
		return select(month,31,28,31,30,31,30,31,31,30,31,30,31)
	end
end

local function julian_check(year,month,day)
	if month < 1 or month > 12 then
		return false
	end
	if day < 1 or day > julian_days_in_month(year,month) then
		return false
	end
	return true
end

--[[--- Convertion ---]]

-- Return Hijri Date from Gregorian date
local function gregorian2hijri(gyear, gmonth, gday)
	return jd2hijri(gregorian2jd(gyear, gmonth, gday))
end

local function hijri2gregorian(hyear, hmonth, hday)
	return jd2gregorian(hijri2jd(hyear, hmonth, hday))
end

--[[------------------- D A T E   C L A S S ------------------------]]

local datePrototype = {}

-- Set julianDay
function datePrototype:set_jd(arg_jd)
	if self.p.type == 'hijri' then
		self.p.year, self.p.month, self.p.day, self.p.yday = jd2hijri(arg_jd,self.p.mode)
		self.p.jd = arg_jd
	elseif self.p.type == 'gregorian' or (self.p.type == 'western' and arg_jd >= gregorian_epoch.jd ) then
		self.p.year, self.p.month, self.p.day = jd2gregorian(arg_jd)
		self.p.jd, self.p.yday = arg_jd, nil
	elseif self.p.type == 'julian' or (self.p.type == 'western' and arg_jd < gregorian_epoch.jd ) then
		self.p.year, self.p.month, self.p.day = jd2julian(arg_jd)
		self.p.jd, self.p.yday = arg_jd, nil
	end
	self.p.wday, self.p.month_days, self.p.leap_year  = nil, nil, nil
end

function datePrototype:get_jd()
	if self.p.jd ~= nil then
		return self.p.jd
	elseif self.p.year and self.p.month and self.p.day then
		self:set_date(self.p.year,self.p.month,self.p.day)
	else
		local t=os.date("*t")
		if self.p.type=='gregorian' then
			self.p.year, self.p.month, self.p.day, self.p.wday, self.p.yday = t.year, t.month, t.day, t.wday - 1 + wday_sunday, t.yday
			self.p.jd = gregorian2jd(self.p.year, self.p.month, self.p.day)
		else
			self:from_gregorian(t.year,t.month,t.day)
			self.p.wday = t.wday -1 + wday_sunday
		end
	end
	return self.p.jd
end

function datePrototype:set_timestamp(arg_tm)
	if self.p.timestamp == arg_tm then
		return
	end
	self.p.timestamp = arg_tm
	self:set_jd(unix2jd(arg_tm))
end

    -- adjust type of western calendar
local function adj_type(type,year,month,day)
	if type == 'western' then
		if year>gregorian_epoch.y or (year==gregorian_epoch.y and month>gregorian_epoch.m) or (year==gregorian_epoch.y and month == gregorian_epoch.m and day>=gregorian_epoch.d) then
			type='gregorian'
		else
			type = 'julian'
		end
	end
	return type
end

	-- Check the Date
local function check_date(ctype,cmode,year,month,day,may_adjusted)
	local type = adj_type(ctype,year,month,day)
	if month <1 or month >12 or day <1 or day >31 then
		return false
	elseif type == 'hijri' then
		if day > 30 or (not may_adjusted and day> hijri_days_in_month(year,month,cmode)) then
			return false
		end
	elseif day > select(month,31,29,31,30,31,30,31,31,30,31,30,31) then 
		return false
	elseif month == 2 and day == 29 then
		if (((year % 4) ~= 0) or (type == 'gregorian'  and (year % 100) == 0 and (year % 400) ~= 0)) then
			return false
		end
	end
	return true
end

	-- Set Date
	-- may_adjusted argument used in Hijri only to accept any 30 day of any month
function datePrototype:set_date(year,month,day,may_adjusted)
	if not check_date(self.p.type, self.p.mode, year, month, day, may_adjusted) then
		error("Invalid Date")
	end
	local type=adj_type(self.p.type,year,month,day)
	if type == 'hijri' then
		self.p.jd = hijri2jd(year,month,day,self.p.mode)
	elseif type == 'gregorian' then
		self.p.jd = gregorian2jd(year,month,day)
	elseif type == 'julian' then
		self.p.jd = julian2jd(year,month,day)
	end
	self.p.year, self.p.month, self.p.day = year,month,day
	self.p.wday, self.p.month_days, self.p.leap_year, self.p.yday  = nil, nil, nil, nil
end

-- Convert date from hijri
function datePrototype:from_hijri(year,month,day,wday,mode)
	mode = mode or self.p.mode
	local jd = hijri2jd(year,month,day,mode)
	if wday and wday <= 6 and wday >=0 then
		local jd_weekday = jd2wday(jd)
		local differ = wday - jd_weekday

		if differ < - 4 then
			jd = jd + (differ + 7)
		elseif differ > -3 and differ < 3 then
			jd = jd + differ
		elseif differ > 4 then
			jd = jd + (differ - 7)
		else
			error("Weekday is very far from Hijri date")
		end
	end
	if self.p.type == 'hijri' and check_date(self.p.type, self.p.mode, year,month,day,true) then
		self.p.year,self.p.month,self.p.day,self.p.jd=year,month,day,jd
		self.p.wday, self.p.month_days, self.p.leap_year, self.p.yday  = nil, nil, nil, nil
	else
		self:set_jd(jd)
	end
end

-- Convert date from Gregorian
function datePrototype:from_gregorian(year,month,day)
	self:set_jd(gregorian2jd(year,month,day))
end
-- Convert date from Julian
function datePrototype:from_julian(year,month,day)
	self:set_jd(julian2jd(year,month,day))
end

-- Convert date from Western
function datePrototype:from_western(year,month,day)
	if year>gregorian_epoch.y or (year==gregorian_epoch.y and month>gregorian_epoch.m) or (year==gregorian_epoch.y and month == gregorian_epoch.m and day>=gregorian_epoch.d) then
		self:set_jd(gregorian2jd(year,month,day))
	else
		self:set_jd(julian2jd(year,month,day))
	end
end



-- set type of calendar
function datePrototype:set_type(arg_type)
	local new_type,new_mode
	arg_type = string.lower(arg_type)
	if in_array(arg_type, {'hijri', 'hijri_adjusted_umalqura', 'hijri_umalqura', 'hijri_tabular', 'gregorian', 'julian', 'western'}) then
		if string.sub(arg_type,1,5) == 'hijri' then
			new_type = 'hijri'
			if arg_type == 'hijri_umalqura' then
				new_mode = 2
			elseif arg_type == 'hijri_tabular' then
				new_mode = 0
			else
				new_mode = 1
			end
		else
			new_type = arg_type
		end

		if new_type ~= self.p.type or new_mode ~= self.p.mode then
			self.p.type = new_type
			self.p.mode = new_mode
			if self.p.jd then
				self:set_jd(self.p.jd)
			end
		end
	else
		error('bad calendar type')
	end
end

function datePrototype:get_value(key)
	if not self.p[key] then
		local jd = self:get_jd() -- to init calendar if not
		if key=='wday' then
			self.p.wday = jd2wday(jd)
		elseif key == 'timestamp' then
			self.p.timestamp = jd2unix(jd)
		else
			if self.p.type=='hijri' and key=='leap_year' then
				self.p.leap_year = hijri_isleap(self.p.year,self.p.mode)
			elseif self.p.type == 'gregorian' or (self.p.type == 'western' and self.p.year > gregorian_epoch.y ) then
				self.p.leap_year = gre_isleap(self.p.year)
			elseif self.p.type == 'julian' or (self.p.type == 'western' and self.p.year <= gregorian_epoch.y ) then
				self.p.leap_year = julian_isleap(self.p.year)
			end
	
			if key == 'yday' then
				if  self.p.type ~= 'hijri' then
					if self.p.leap_year then
						self.p.yday =  select(self.p.month,0,31,60,91,121,152,182,213,244,274,305,335) + self.p.day - 1 + yday_1_1
					else
						self.p.yday =  select(self.p.month,0,31,59,90,120,151,181,212,243,273,304,334) + self.p.day - 1 + yday_1_1
					end
						
				elseif self.p.type == 'hijri' then
					self.p.yday = hijri_yday(self.p.year,self.p.month,self.p.day,self.p.mode)
				end
			elseif key == 'month_days' then
				if self.p.type=='hijri' then
					self.p.month_days = hijri_days_in_month(self.p.year,self.p.month,self.p.mode)
				else
					if self.p.month == 2 and self.p.leap_year then
						self.p.month_days = 29
					else
						self.p.month_days = select(self.p.month,31,28,31,30,31,30,31,31,30,31,30,31)
					end
				end
			end
		end
	end
	return self.p[key]
end

-- add interval to date
function datePrototype:add(y, m, d)
	y,m,d = tonumber(y) or 0,tonumber(m) or 0,tonumber(d) or 0
	local jd = self:get_jd()
	if y==0 and m == 0 then
		jd = jd + d
	else
		local year,month,day=self.p.year + y, self.p.month + m, self.p.day + d
		if self.p.type == 'hijri' then
			jd = hijri2jd(year,month,day,self.p.mode)
		elseif self.p.type == 'gregorian' then
			jd = gregorian2jd(year,month,day)
		elseif self.p.type == 'julian' then
			jd = julian2jd(year,month,day)
		elseif self.p.type == 'western' then
			if self.p.jd >= gregorian_epoch.jd then
				jd = gregorian2jd(year,month,day)
				if jd< gregorian_epoch.jd then
					jd = julian2jd(year,month,day)
				end
			else
				jd = julian2jd(year,month,day)
				if jd>= gregorian_epoch.jd then
					jd = gregorian2jd(year,month,day)
				end
			end
		end
	end

	if jd ~= self.p.jd then
		self:set_jd(jd)
	end
end


function datePrototype:iso()
	local ret, str, jd = '', '', self:get_jd()
	local year, month, day

	if self.p.type=='gregorian' then
		year, month, day = self.p.year, self.p.month, self.p.day
	else
		year, month, day = jd2gregorian(jd)
	end

	if year < 0 then
		year = year - 1
		ret = '-'
	end
	str = tostring(math.abs(year))
	return ret .. string.rep('0', 4 - #str) .. str .. '-' .. ((month<10) and '0' or '') .. month .. '-' .. ((day<10) and '0' or '') .. day
end

local function DateWrapper(dt)
	local t = {}
	t.p = {}
	local rop = {
		type = false,
		mode = true,
		jd  = false,
		timestamp = false,
		year = true,
		month = true,
		day = true,
		wday = true,
		yday = true,
		month_days = true,
		leap_year  = true
		}

	local mt = {
		__index = function ( tt, k )
			assert( t == tt )
			local v = tt.p[k] or  datePrototype[k]
			if v == nil and rop[k] ~=nil then
				v = tt:get_value(k)
			end
			return v
		end,
		__newindex = function ( t, k, v )
			if rop[k] then
				error("Attempt to modify read-only property")
			elseif k == 'jd' then
				t:set_jd(v)
			elseif k == 'type' then
				t:set_type(v)
			elseif k == 'timestamp' then
				t:set_timestamp(v)
			else
				rawset(t, k, v)
			end
		end
	}
	-- This is just to make setmetatable() fail
	mt.__metatable = mt

	return setmetatable( t, mt )
end

local function Date(arg_type, arg_jd, arg_month, arg_day)
	local d = DateWrapper({})
	-- process argument
	d:set_type(arg_type)

	if arg_jd and arg_month and arg_day  then
		d:set_date(arg_jd, arg_month, arg_day)
	elseif arg_jd then
		d:set_jd(arg_jd)
	end
	return d
end


return {
	gregorian2jd = gregorian2jd,
	jd2hijri = jd2hijri,
	hijri2jd = hijri2jd,
	hijri_days_in_month = hijri_days_in_month,
	hijri_isleap = hijri_isleap,
	hijri_yday = hijri_yday,
	jd2unix = jd2unix,
	unix2jd = unix2jd,
	jd2wday = jd2wday,
	jd2gregorian = jd2gregorian,
	gre_isleap = gre_isleap,
	gre_yday = gre_yday,
	gre_days_in_month = gre_days_in_month,
	jd2julian = jd2julian,
	julian2jd = julian2jd,
	julian_isleap = julian_isleap,
	julian_yday = julian_yday,
	julian_days_in_month = julian_days_in_month,
	gregorian2hijri = gregorian2hijri,
	hijri2gregorian = hijri2gregorian,
	Date = Date,
	hijri_check = hijri_check,
	gre_check = gre_check,
	julian_check = julian_check
}