------------------------------------------------------------------
-- VEAF time and date tools
-- By Flogas (2024)
--
-- Features:
-- ---------
-- * Provides a suite of tools to manage date and time information relative to the DCS mission
-- standard lua datetime object is used when appropriate:
-- --> as returned by os.date("*t", 906000490)
-- --> dateTime = { year = 1998, month = 9, day = 16, yday = 259, wday = *unused*, hour = 23, min = 48, sec = 10, isdst = *unused* }
------------------------------------------------------------------
veafTime = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global module settings
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafTime.Id = "TIME"

--- Version.
veafTime.Version = "1.1.3"

-- trace level, specific to this module
--veafTime.LogLevel = "trace"
veaf.loggers.new(veafTime.Id, veafTime.LogLevel)

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Local constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------
local _iSecondsInMinute = 60
local _iSecondsInHour = 3600
local _iSecondsInDay = 86400

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Local tools
-------------------------------------------------------------------------------------------------------------------------------------------------------------
local function _isLeapYear(iYear)
    return iYear % 4 == 0 and (iYear % 100 ~= 0 or iYear % 400 == 0)
end

local function _getDaysInMonth(iMonth, iYear)
    local daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    if iMonth == 2 and _isLeapYear(iYear) then
        return 29
    end
    return daysInMonth[iMonth]
end

local function _getDayOfYear(iDay, iMonth, iYear)
    local iDayOfYear = iDay
    for i = 1, iMonth - 1 do
        iDayOfYear = iDayOfYear + _getDaysInMonth(i, iYear)
    end
    return iDayOfYear
end

local function _adjustDate(dateTime, iDaysOffset)
    local iYear = dateTime.year
    local iMonth = dateTime.month
    local iDay = dateTime.day
    local iDayOfYear = dateTime.yday
    
    -- Handle day changes
    iDay = iDay + iDaysOffset
    iDayOfYear = iDayOfYear + iDaysOffset
    
    -- Handle forward changes
    while true do
        local iDaysInMonth = _getDaysInMonth(iMonth, iYear)
        if (iDay <= iDaysInMonth) then
            break
        end
        iDay = iDay - iDaysInMonth
        iMonth = iMonth + 1
        if (iMonth > 12) then
            iMonth = 1
            iYear = iYear + 1
            -- Reset yday for new year
            iDayOfYear = iDay
        end
    end
    
    -- Handle backward changes
    while (iDay <= 0) do
        iMonth = iMonth - 1
        if (iMonth <= 0) then
            iMonth = 12
            iYear = iYear - 1
        end
        iDay = iDay + _getDaysInMonth(iMonth, iYear)
        -- Adjust yday for previous year
        if (iMonth == 12) then
            iDayOfYear = 365 + (_isLeapYear(iYear) and 1 or 0) + iDay - _getDaysInMonth(iMonth, iYear)
        end
    end
   
    return { year = iYear, month = iMonth, day = iDay, yday = iDayOfYear, hour = dateTime.hour, min = dateTime.min, sec = dateTime.sec }
end

local function _decimalToHoursMinutes(iDecimalHours)
    -- Handle negative hours (can happen with UTC times)
    if (iDecimalHours < 0) then
        iDecimalHours = iDecimalHours + 24
    end
    -- Handle hours >= 24
    iDecimalHours = iDecimalHours % 24

    local iHours = math.floor(iDecimalHours)
    local iMinutes = math.floor((iDecimalHours - iHours) * 60)
    return iHours, iMinutes
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- General date and time tools
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafTime.getMissionDateTime(iAbsTime)
    iAbsTime = iAbsTime or timer.getAbsTime()

    -- Calculate hours, minutes, and remaining seconds
    local iDay = env.mission.date.Day
    local iMonth = env.mission.date.Month
    local iYear = env.mission.date.Year
    local iHour = math.floor(iAbsTime / _iSecondsInHour)
    local iRemainingSeconds = iAbsTime % _iSecondsInHour
    local iMinute = math.floor(iRemainingSeconds / _iSecondsInMinute)
    local iSecond = iRemainingSeconds % _iSecondsInMinute

    -- Handle day rollover
    local iAdditionalDays = math.floor(iHour / 24)
    iHour = iHour % 24

    -- Add the additional days
    iDay = iDay + iAdditionalDays

    -- Handle month and year rollover
    while true do
        local iDaysInCurrentMonth = _getDaysInMonth(iMonth, iYear)

        if (iDay <= iDaysInCurrentMonth) then
            break
        end

        iDay = iDay - iDaysInCurrentMonth
        iMonth = iMonth + 1

        if iMonth > 12 then
            iMonth = 1
            iYear = iYear + 1
        end
    end

    local idayOfYear = _getDayOfYear(iDay, iMonth, iYear)

    return { year = iYear, month = iMonth, day = iDay, yday = idayOfYear, hour = iHour, min = iMinute, sec = iSecond }
end

function veafTime.getMissionAbsTime(dateTime)
    local iMissionStartDay = env.mission.date.Day
    local iMissionStartMonth = env.mission.date.Month
    local iMissionStartYear = env.mission.date.Year
    
    local iDay = dateTime.day
    local iMonth = dateTime.month
    local iYear = dateTime.year
    local iHour = dateTime.hour
    local iMinute = dateTime.min
    local iSecond = dateTime.sec

    local iTotalDays = 0
    
    -- If we're in the same year
    if iMissionStartYear == iYear then
        iTotalDays = _getDayOfYear(iDay, iMonth, iYear) - _getDayOfYear(iMissionStartDay, iMissionStartMonth, iMissionStartYear)
    else
        -- Days remaining in the first year
        iTotalDays = (_isLeapYear(iMissionStartYear) and 366 or 365) - _getDayOfYear(iMissionStartDay, iMissionStartMonth, iMissionStartYear)
        
        -- Add days for full years in between
        for iYear = iMissionStartYear + 1, iYear - 1 do
            iTotalDays = iTotalDays + (_isLeapYear(iYear) and 366 or 365)
        end
        
        -- Add days in the final year
        iTotalDays = iTotalDays + _getDayOfYear(iDay, iMonth, iYear)
    end
    
    -- Calculate total seconds
    local iAbsTime = iSecond +
                    (iMinute * _iSecondsInMinute) +
                    (iHour * _iSecondsInHour) +
                    (iTotalDays * _iSecondsInDay)
    
    return iAbsTime
end

function veafTime.absTimeToDateTime(iAbsTime)
    iAbsTime = iAbsTime or timer.getAbsTime()
    return veafTime.getMissionDateTime(iAbsTime)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Date and time string display tools
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafTime.toStringDate(dateTime)
    return string.format("%02d/%02d/%d", dateTime.day, dateTime.month, dateTime.year)
end

function veafTime.absTimeToStringDate(iAbsTime)
    local dateTime = veafTime.absTimeToDateTime(iAbsTime)
    return veafTime.toStringDate(dateTime)
end

function veafTime.toStringTime(dateTime, bWithSeconds)
    if (bWithSeconds == nil) then bWithSeconds = true end

    --veaf.loggers.get(veafTime.Id):trace(veaf.p(dateTime))

    if (bWithSeconds) then
        return string.format("%02d:%02d:%02d", dateTime.hour, dateTime.min, dateTime.sec)
    else
        return string.format("%02d:%02d", dateTime.hour, dateTime.min)
    end
end

function veafTime.absTimeToStringTime(iAbsTime, bWithSeconds)
    local dateTime = veafTime.absTimeToDateTime(iAbsTime)
    return veafTime.toStringTime(dateTime, bWithSeconds)
end

function veafTime.toStringDateTime(dateTime, bWithSeconds)
    return string.format("%s %s", veafTime.toStringDate(dateTime), veafTime.toStringTime(dateTime, bWithSeconds))
end

function veafTime.absTimeToStringDateTime(iAbsTime)
    local dateTime = veafTime.absTimeToDateTime(iAbsTime)
    return veafTime.toStringDateTime(dateTime, bWithSeconds)
end

-- Helper function to determine season based on latitude and month
function veafTime.determineSeason(month, latitude)
    -- Determine if in Northern or Southern Hemisphere
    local isNorthernHemisphere = latitude == nil or latitude >= 0

    -- Season mapping adjusted for hemisphere
    if isNorthernHemisphere then
        -- Northern Hemisphere seasons
        if month >= 3 and month <= 5 then
            return "spring"
        elseif month >= 6 and month <= 8 then
            return "summer"
        elseif month >= 9 and month <= 11 then
            return "autumn"
        else
            return "winter"
        end
    else
        -- Southern Hemisphere seasons (reversed)
        if month >= 3 and month <= 5 then
            return "autumn"
        elseif month >= 6 and month <= 8 then
            return "winter"
        elseif month >= 9 and month <= 11 then
            return "spring"
        else
            return "summer"
        end
    end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Timezones
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafTime.getTimezone(vec3)
    local nTimezoneOffset = 0
    local sTheatre = env.mission.theatre

    if (vec3) then
        -- Try to approximate for the vec3 - each timezone is roughly 15 degrees wide
        local nLatitude, nLongitude, _ = coord.LOtoLL(vec3)
        nTimezoneOffset = math.floor((nLongitude + 7.5) / 15)
    elseif (sTheatre == veaf.theatreName.Caucasus) then
        nTimezoneOffset = 4
    elseif (sTheatre == veaf.theatreName.PersianGulf) then
        nTimezoneOffset = 4
    elseif (sTheatre == veaf.theatreName.Nevada) then
        nTimezoneOffset = -8 -- Nevada uses DST (UTC-7 march through october) but it is not modeled in DCS
    elseif (sTheatre == veaf.theatreName.Normandy) then
        nTimezoneOffset = 0
    elseif (sTheatre == veaf.theatreName.TheChannel) then
        nTimezoneOffset = 2
    elseif (sTheatre == veaf.theatreName.Syria) then
        nTimezoneOffset = 3
    elseif (sTheatre == veaf.theatreName.MarianaIslands) then
        nTimezoneOffset = 10
    elseif (sTheatre == veaf.theatreName.Falklands) then
        nTimezoneOffset = -3
    elseif (sTheatre == veaf.theatreName.Sinai) then
        nTimezoneOffset = 2
    elseif (sTheatre == veaf.theatreName.Kola) then
        nTimezoneOffset = 3
    elseif (sTheatre == veaf.theatreName.Afghanistan) then
        nTimezoneOffset = 4.5
    end
    --veaf.loggers.get(veafTime.Id):trace(string.format("%s - timezone=%f", env.mission.theatre, nTimezoneOffset))
    return nTimezoneOffset
end

function veafTime.toZulu(dateTime, nOffsetHours)
    nOffsetHours = nOffsetHours or veafTime.getTimezone()
    
    -- Create a new table to avoid modifying the original
    local dateTimeZulu = {}
    for k, v in pairs(dateTime) do
        dateTimeZulu[k] = v
    end
    
    -- Convert hours and handle day boundary changes
    local iTotalMinutes = dateTimeZulu.hour * 60 + dateTimeZulu.min - (nOffsetHours * 60)
    local iDays = math.floor(iTotalMinutes / (24 * 60))
    iTotalMinutes = iTotalMinutes % (24 * 60)
    
    -- Update hours and minutes
    dateTimeZulu.hour = math.floor(iTotalMinutes / 60)
    dateTimeZulu.min = iTotalMinutes % 60
    
    -- Handle date changes if needed
    if iDays ~= 0 then
        return _adjustDate(dateTimeZulu, iDays)
    end
    
    return dateTimeZulu
end

function veafTime.toLocal(utcDateTime, nOffsetHours)
    nOffsetHours = nOffsetHours or veafTime.getTimezone()
    return veafTime.toZulu(utcDateTime, -nOffsetHours)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Sun times
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafTime.getSunTimesZulu(vec3, iAbsTime)
    local nLatitude, nLongitude, _ = coord.LOtoLL(vec3)
    local date = veafTime.absTimeToDateTime(iAbsTime)

    return veafSunTimes.getSunTimes(nLatitude, nLongitude, date, 0)
end

function veafTime.getSunTimesLocal(vec3, iAbsTime)
    local nLatitude, nLongitude, _ = coord.LOtoLL(vec3)
    local date = veafTime.absTimeToDateTime(iAbsTime)
    
    return veafSunTimes.getSunTimes(nLatitude, nLongitude, date, veafTime.getTimezone())
end

function veafTime.isAeronauticalNight(vec3, iAbsTime)
    local dateTime = veafTime.absTimeToDateTime(iAbsTime)
    local sunTimes = veafTime.getSunTimesLocal(vec3, iAbsTime)
    local sunriseTime = sunTimes.Sunrise
    local sunsetTime = sunTimes.Sunset
    --veaf.loggers.get(veafTime.Id):trace(veaf.p(sunriseTime))
    --veaf.loggers.get(veafTime.Id):trace(veaf.p(sunsetTime))

    local iCurrentSeconds = dateTime.hour * _iSecondsInHour + dateTime.min * _iSecondsInMinute + dateTime.sec
    local iSunriseSeconds = sunriseTime.hour * _iSecondsInHour + sunriseTime.min * _iSecondsInMinute + sunriseTime.sec - (30 * _iSecondsInMinute) -- sunrise - 30 min
    local iSunsetSeconds = sunsetTime.hour * _iSecondsInHour + sunsetTime.min * _iSecondsInMinute + sunsetTime.sec + (30 * _iSecondsInMinute) -- sunset + 30 min
    
    --veaf.loggers.get(veafTime.Id):trace(string.format("iCurrentSeconds=%d  iSunriseSeconds=%d   iSunsetSeconds=%d", iCurrentSeconds, iSunriseSeconds, iSunsetSeconds))
    
    return iCurrentSeconds < iSunriseSeconds or iCurrentSeconds > iSunsetSeconds
end

local dawnAngle, duskAngle = 6, 6
local DR = math.pi / 180
local K1 = 15 * math.pi * 1.0027379 / 180

veafSunTimes = {}
veafSunTimes.__index = veafSunTimes

---------------------------------------------------------------------------------------------------
---  CTOR
function veafSunTimes:create()
    local this = {
        sunRiseSetTimes = {},
        moonRiseSetTimes = {},
        NoSunRise = false,
        NoSunSet = false,
        lat = 0,
        long = 0,
        timeOffset = 0,
        jDateSun = nil
    }

    setmetatable(this, veafSunTimes)
    return this
end

----------------------------------------------------------------------------------------------------
-- Main static method
----------------------------------------------------------------------------------------------------
function veafSunTimes.getSunTimes(nLatitude, nLongitude, date, nTimeZone)
    local sunTimes = veafSunTimes:create()
    sunTimes:compute(nLatitude, nLongitude, nTimeZone, date)

    local sunrise = sunTimes.sunRiseSetTimes[2]
    local sunset = sunTimes.sunRiseSetTimes[3]

    local iSunriseHour, iSunriseMinute = _decimalToHoursMinutes(sunrise)
    local iSunsetHour, iSunsetMinute = _decimalToHoursMinutes(sunset)

    return
    {
        Sunrise = { year = date.year, month = date.month, day = date.day, yday = date.yday, hour = iSunriseHour, min = iSunriseMinute, sec = 0 },
        Sunset =  { year = date.year, month = date.month, day = date.day, yday = date.yday, hour = iSunsetHour, min = iSunsetMinute, sec = 0 }
    }
end

----------------------------------------------------------------------------------------------------
-- Main compute method
----------------------------------------------------------------------------------------------------
function veafSunTimes:compute(nLatitude, nLongitude, nTimeZone, date)
    self.sunRiseSetTimes = {6, 6, 6, 12, 13, 18, 18, 18, 24}
    self.moonRiseSetTimes = {0, 23.9}
    self.NoSunRise = false
    self.NoSunSet = false
    self.lat = nLatitude
    self.long = nLongitude
    self.timeOffset = nTimeZone
    self.jDateSun = self:julian(date.year, date.month, date.day) - (nLongitude / (15 * 24))

    -- sun time calculations
    self:calcSunRiseSet()
    if self.NoSunRise or self.NoSunSet then
        -- adjust times to solar noon
        self.sunRiseSetTimes[2] = (self.sunRiseSetTimes[2] - 12)
        if self.NoSunRise then
            self.sunRiseSetTimes[3] = self.sunRiseSetTimes[2] + 0.0001
        else
            self.sunRiseSetTimes[3] = (self.sunRiseSetTimes[2] - 0.0001)
        end
        self.sunRiseSetTimes[1] = 0
        self.sunRiseSetTimes[4] = 0
    end

    -- debugging
    -- print("dawn = "       .. TimeString(sunRiseSetTimes[1],  nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[1]  ..")")
    -- print("sunrise = "    .. TimeString(sunRiseSetTimes[2],  nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[2]  ..")")
    -- print("sunset = "     .. TimeString(sunRiseSetTimes[3],  nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[3]  ..")")
    -- print("twilight = "   .. TimeString(sunRiseSetTimes[4],  nTimeLZero, nTimeStyle) .. " (" .. sunRiseSetTimes[4]  ..")")
end

----------------------------------------------------------------------------------------------------
-- Following code kept as close as possible to https://gist.github.com/eDave56/6dfae1b62c4cf743afe0ad61e300f091
----------------------------------------------------------------------------------------------------

------------------------------------ [ sun time calculations ] -------------------------------------

function veafSunTimes:midDay(Ftime)
    local eqt = self:sunPosition(self.jDateSun + Ftime, 0)
    local noon = veafSunTimes.DMath.fixHour(12 - eqt)
    return noon
end -- function midDay

function veafSunTimes:sunAngleTime(angle, Ftime, direction)
    --
    -- time at which sun reaches a specific angle below horizon
    --
    local decl = self:sunPosition(self.jDateSun + Ftime, 1)
    local noon = self:midDay(Ftime)
    local t = (-veafSunTimes.DMath.Msin(angle) - veafSunTimes.DMath.Msin(decl) * veafSunTimes.DMath.Msin(self.lat)) / (veafSunTimes.DMath.Mcos(decl) * veafSunTimes.DMath.Mcos(self.lat))

    if t > 1 then
        -- the sun doesn't rise today
        self.NoSunRise = 1
        return noon
    elseif t < -1 then
        -- the sun doesn't set today
        self.NoSunSet = 1
        return noon
    end

    t = 1 / 15 * veafSunTimes.DMath.arccos(t)
    return noon + ((direction == "CCW") and -t or t)
end -- function sunAngleTime

function veafSunTimes:sunPosition(jd, Declination)
    --
    -- compute declination angle of sun
    --
    local D = jd - 2451545
    local g = veafSunTimes.DMath.fixAngle(357.529 + 0.98560028 * D)
    local q = veafSunTimes.DMath.fixAngle(280.459 + 0.98564736 * D)
    local L = veafSunTimes.DMath.fixAngle(q + 1.915 * veafSunTimes.DMath.Msin(g) + 0.020 * veafSunTimes.DMath.Msin(2 * g))
    local R = 1.00014 - 0.01671 * veafSunTimes.DMath.Mcos(g) - 0.00014 * veafSunTimes.DMath.Mcos(2 * g)
    local e = 23.439 - 0.00000036 * D
    local RA = veafSunTimes.DMath.arctan2(veafSunTimes.DMath.Mcos(e) * veafSunTimes.DMath.Msin(L), veafSunTimes.DMath.Mcos(L)) / 15
    local eqt = q / 15 - veafSunTimes.DMath.fixHour(RA)
    local decl = veafSunTimes.DMath.arcsin(veafSunTimes.DMath.Msin(e) * veafSunTimes.DMath.Msin(L))

    if Declination == 1 then
        return decl
    else
        return eqt
    end
end -- function sunPosition

function veafSunTimes:julian(year, month, day)
    --
    -- convert Gregorian date to Julian day
    --
    if (month <= 2) then
        year = year - 1
        month = month + 12
    end
    local A = math.floor(year / 100)
    local B = 2 - A + math.floor(A / 4)
    local JD = math.floor(365.25 * (year + 4716)) + math.floor(30.6001 * (month + 1)) + day + B - 1524.5
    return JD
end -- function julian

function veafSunTimes:setTimes(sunRiseSetTimes)
    local Ftimes = self:dayPortion(sunRiseSetTimes)
    local dawn = self:sunAngleTime(dawnAngle, Ftimes[2], "CCW")
    local sunrise = self:sunAngleTime(self:riseSetAngle(), Ftimes[3], "CCW")
    local sunset = self:sunAngleTime(self:riseSetAngle(), Ftimes[8], "CW")
    local dusk = self:sunAngleTime(duskAngle, Ftimes[7], "CW")
    return {dawn, sunrise, sunset, dusk}
end -- function setTimes

function veafSunTimes:calcSunRiseSet()
    self.sunRiseSetTimes = self:setTimes(self.sunRiseSetTimes)
    return self:adjustTimes(self.sunRiseSetTimes)
end      

function veafSunTimes:adjustTimes(sunRiseSetTimes)
    for i = 1, #sunRiseSetTimes do
        sunRiseSetTimes[i] = sunRiseSetTimes[i] + (self.timeOffset - self.long / 15)
    end
    sunRiseSetTimes = self:adjustHighLats(sunRiseSetTimes)
    return sunRiseSetTimes
end -- function adjustTimes

function veafSunTimes:riseSetAngle()
    --
    -- sun angle for sunset/sunrise
    --
    -- local angle = 0.0347 * math.sqrt( elv )
    local angle = 0.0347
    return 0.833 + angle
end -- function riseSetAngle

function veafSunTimes:adjustHighLats(sunRiseSetTimes)
    --
    -- adjust times for higher latitudes
    --
    local nightTime = self:timeDiff(sunRiseSetTimes[3], sunRiseSetTimes[2])
    sunRiseSetTimes[1] = self:refineHLtimes(sunRiseSetTimes[1], sunRiseSetTimes[2], (dawnAngle), nightTime, "CCW")
    return sunRiseSetTimes
end -- function adjustHighLats

function veafSunTimes:refineHLtimes(Ftime, base, angle, night, direction)
    --
    -- refine time for higher latitudes
    --
    local portion = night / 2
    local FtimeDiff = (direction == "CCW") and self:timeDiff(Ftime, base) or self:timeDiff(base, Ftime)
    if not ((Ftime * 2) > 2) or (FtimeDiff > portion) then
        Ftime = base + ((direction == "CCW") and -portion or portion)
    end
    return Ftime
end -- function refineHLtimes

function veafSunTimes:dayPortion(sunRiseSetTimes)
    --
    --  convert hours to day portions
    --
    for i = 1, #sunRiseSetTimes do
        sunRiseSetTimes[i] = sunRiseSetTimes[i] / 24
    end
    return sunRiseSetTimes
end -- function dayPortion

function veafSunTimes:timeDiff(time1, time2)
    --
    --  difference between two times
    --
    return veafSunTimes.DMath.fixHour(time2 - time1)
end -- function timeDiff

----------------------------------- [ moon time calaculations ] ------------------------------------
-- VEAF not retained
------------------------------------- [ other odds and sods ] --------------------------------------
-- VEAF not retained

---------------------------------------- [ math functions ] ----------------------------------------

function veafSunTimes.fix(a, b)
    a = a - b * (math.floor(a / b))
    return (a < 0) and a + b or a
end

function veafSunTimes.dtr(d)
    return (d * math.pi) / 180
end
function veafSunTimes.rtd(r)
    return (r * 180) / math.pi
end

veafSunTimes.DMath = {
    Msin = function(d)
        return math.sin(veafSunTimes.dtr(d))
    end,
    Mcos = function(d)
        return math.cos(veafSunTimes.dtr(d))
    end,
    Mtan = function(d)
        return math.tan(veafSunTimes.dtr(d))
    end,
    arcsin = function(d)
        return veafSunTimes.rtd(math.asin(d))
    end,
    arccos = function(d)
        return veafSunTimes.rtd(math.acos(d))
    end,
    arctan = function(d)
        return veafSunTimes.rtd(math.atan(d))
    end,
    arccot = function(x)
        return veafSunTimes.rtd(math.atan(1 / x))
    end,
    arctan2 = function(y, x)
        return veafSunTimes.rtd(math.atan2(y, x))
    end,
    fixAngle = function(a)
        return veafSunTimes.fix(a, 360)
    end,
    fixHour = function(a)
        return veafSunTimes.fix(a, 24)
    end
}

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
-------------------- TEST STUFF--------------------------------------------------------------------
--[[
local _TEST_LOCATIONS =
{
    ["BATUMI"] = {41.6167547, 41.6367455, 4},
    ["DAMASCUS"] = {34.802075, 38.996815, 3},
    ["SAIPAN"] = {15.0979, 145.6739, 10},
    ["LAS VEGAS"] = {36.176, -115.137, -8},
    ["DUBAI"] = {23.424076, 53.847818, 4}
}

local function _TEST_SUN_TIMES(sLocation, date, sExpectedRise, sExpectedSet)
    local nLatitude = _TEST_LOCATIONS[sLocation][1]
    local nLongitude = _TEST_LOCATIONS[sLocation][2]
    local iTimezoneOffset = veafTime.getTimezone()
    
    if (date == nil) then
        date = veafTime.absTimeToDateTime()
        sExpectedRise = "no data"
        sExpectedSet = "no data"
    else
        date = {day = date[1], month = date[2], year = date[3]}
    end

    local sunTimes = veafSunTimes.getSunTimes(nLatitude, nLongitude, date, iTimezoneOffset)

    local sunrise = sunTimes.Sunrise
    local sunset = sunTimes.Sunset
    veaf.loggers.get(veafTime.Id):trace("%s ; %02d/%02d/%d : sunrise=%02d:%02d, sunset=%02d:%02d ( expected rise=%s, set=%s )", sLocation, date.day, date.month, date.year, sunrise.hour, sunrise.min, sunset.hour, sunset.min, sExpectedRise, sExpectedSet)
end

veaf.loggers.get(veafTime.Id):trace("%s >> TESTS TESTS TESTS", veafTime.Id)
local sTheatre = string.lower(env.mission.theatre)
local sLoc
if (sTheatre == "caucasus") then
    sLoc = "BATUMI"
    _TEST_SUN_TIMES(sLoc, 1, 1, 2024, "08:40", "17:53")
    _TEST_SUN_TIMES(sLoc, 1, 4, 2024, "06:54", "19:39")
    _TEST_SUN_TIMES(sLoc, 1, 7, 2024, "05:43", "20:51")
    _TEST_SUN_TIMES(sLoc, 1, 10, 2024, "07:11", "18:54")
elseif (sTheatre == "persiangulf") then
    sLoc = "DUBAI"
    _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "07:06", "17:49")
    _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:16", "18:40")
    _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:42", "19:15")
    _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:16", "18:11")    
elseif (sTheatre == "nevada") then
    sLoc = "LAS VEGAS"
    _TEST_SUN_TIMES(sLoc)
    _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "06:50", "16:37")
    _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:24 (05:24 w/o DST)", "19:04 (18:04 w/o DST)")
    _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:25 (04:25 w/o DST)", "20:14 (19:14 w/o DST)")
    _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:43 (05:43 w/o DST)", "18:26 (17:25 w/o DST)")    
elseif (sTheatre == "normandy") then
    sLoc = ""
elseif (sTheatre == "thechannel") then
    sLoc = ""
elseif (sTheatre == "syria") then
    sLoc = "DAMASCUS"
    _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "07:38", "17:37")
    _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:22", "18:56")
    _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:28", "19:46")
    _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:29", "18:18")    
elseif (sTheatre == "marianaislands") then
    sLoc = "SAIPAN"
    _TEST_SUN_TIMES(sLoc, {1, 1, 2024}, "06:42", "17:57")
    _TEST_SUN_TIMES(sLoc, {1, 4, 2024}, "06:12", "18:29")
    _TEST_SUN_TIMES(sLoc, {1, 7, 2024}, "05:51", "18:51")
    _TEST_SUN_TIMES(sLoc, {1, 10, 2024}, "06:07", "18:06")    
elseif (sTheatre == "falklands") then
    sLoc = ""
elseif (sTheatre == "sinaiMap") then
    sLoc = ""
elseif (sTheatre == "kola") then
    sLoc = ""
elseif (sTheatre == "afghanistan") then
    sLoc = ""
end
]]