module Data.UTC.Internal
  ( daysToYearMonthDay
  , yearMonthDayToDays

  , yearToDays
  , daysToYear

  , deltaUnixEpochCommonEpoch

  , isValidDate

  , secsPerDay, secsPerHour, secsPerMinute, minsPerHour, hoursPerDay
  , monthsPerYear
  ) where

deltaUnixEpochCommonEpoch :: Rational
deltaUnixEpochCommonEpoch
  = 62167219200

secsPerDay :: Integer
secsPerDay
  = hoursPerDay * secsPerHour

secsPerHour :: Integer
secsPerHour
  = minsPerHour * secsPerMinute

secsPerMinute :: Integer
secsPerMinute
  = 60

minsPerHour :: Integer
minsPerHour
  = 60

hoursPerDay :: Integer
hoursPerDay
  = 24

monthsPerYear :: Integer
monthsPerYear
  = 12

-- | Convert Year-Month-Day to since 0000-01-01 in the Gregorian Calendar
--
--    * year   0         is     a leap year
--    * year 400         is     a leap year
--    * year 100,200,300 is not a leap year
--    * year / 4         is     a leap year
--    * year (-4) (5 BC) is     a leap year

--    * AD/BC vs. astronomical year numbering (as used by ISO8601)
--    * year   0         ia 1 BC
--    * year (-1)        is 2 BC
--    * year (-2)        is 3 BC etc.

yearMonthDayToDays :: (Integer, Integer, Integer) -> Integer
yearMonthDayToDays (year,month,day)
  = -- count of days of the "finalised" years
    let daysY = yearToDays year
    -- count of days of the "finalised" months
        daysM = case month - 1 of
                  1  -> 31
                  2  -> 31 + 28 + leapDay
                  3  -> 31 + 28 + 31 + leapDay
                  4  -> 31 + 28 + 31 + 30 + leapDay
                  5  -> 31 + 28 + 31 + 30 + 31 + leapDay
                  6  -> 31 + 28 + 31 + 30 + 31 + 30 + leapDay
                  7  -> 31 + 28 + 31 + 30 + 31 + 30 + 31 + leapDay
                  8  -> 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + leapDay
                  9  -> 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + leapDay
                  10 -> 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + leapDay
                  11 -> 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + leapDay
                  _  -> 0
       -- count of the "finalised" days
        daysD = day - 1
    in  daysY + daysM + daysD
  where
    leapDay :: Integer
    leapDay
      | (year `mod` 4 == 0) && ((year `mod` 400 == 0) || (year `mod` 100 /= 0)) = 1
      | otherwise                                                               = 0

yearToDays :: Integer -> Integer
yearToDays y
  | y == 0    = 0
  | y >= 0    = 366
              + ((y-1) * 365)       -- .. and a normal year has 365 days ..
              + ((y-1) `quot` 4)    -- .. and every 4 years a leap day occurs..
              - ((y-1) `quot` 100)  -- .. but not in centuries ..
              + ((y-1) `quot` 400)  -- .. except every 400 years.
  | otherwise = (y * 365)
              + (y `quot` 4)
              - (y `quot` 100)
              + (y `quot` 400)

daysToYear :: Integer -> Integer
daysToYear ds
  = if ds <= d
      then y - 1
      else y
  where
    y = ds `div` 365
    d = yearToDays y

daysToYearMonthDay :: Integer -> (Integer, Integer, Integer)
daysToYearMonthDay d'
  = (year + y400 * 400, month, day)
  where
    d400            = yearToDays 400
    y400            = d' `div` d400
    d               = d' `mod` d400
    year            = daysToYear (d + 1)
    ld              = if (year `mod` 4   == 0) &&
                        ((year `mod` 400 == 0)
                      || (year `mod` 100 /= 0)) then (1+) else id
    doy             = d - yearToDays year
    (month,day) 
     | doy <     31 = ( 1, doy          + 1)
     | doy < ld  59 = ( 2, doy -     31 + 1)
     | doy < ld  90 = ( 3, doy - ld  59 + 1)
     | doy < ld 120 = ( 4, doy - ld  90 + 1)
     | doy < ld 151 = ( 5, doy - ld 120 + 1)
     | doy < ld 181 = ( 6, doy - ld 151 + 1)
     | doy < ld 212 = ( 7, doy - ld 181 + 1)
     | doy < ld 243 = ( 8, doy - ld 212 + 1)
     | doy < ld 273 = ( 9, doy - ld 243 + 1)
     | doy < ld 304 = (10, doy - ld 273 + 1)
     | doy < ld 334 = (11, doy - ld 304 + 1)
     | otherwise    = (12, doy - ld 334 + 1)

isValidDate :: (Integer, Integer, Integer) -> Bool
isValidDate (y,m,d)
  | m < 1  = False
  | m > 12 = False
  | otherwise = case m of
                   1  -> validateDays31
                   2  -> validateDays28or29
                   3  -> validateDays31
                   4  -> validateDays30
                   5  -> validateDays31
                   6  -> validateDays30
                   7  -> validateDays31
                   8  -> validateDays31
                   9  -> validateDays30
                   10 -> validateDays31
                   11 -> validateDays30
                   12 -> validateDays31
                   _  -> False
  where
    validateDays31
      | 1 <= d && d <= 31           = True
      | otherwise                   = False
    validateDays30
      | 1 <= d && d <= 30           = True
      | otherwise                   = False
    validateDays28or29
      | 1 <= d && d <= 28           = True
      | d == 29 && isLeapYear       = True
      | otherwise                   = False
    isLeapYear
      = (y `mod` 4 == 0) && ((y `mod` 400 == 0) || (y `mod` 100 /= 0))