{-|
Module: Utils.Holidays
Description: German bank holidays

Interfaces follows module Data.Time.Calendar.BankHoliday.EnglandAndWales 
isBankHolidayArea uses laziness to provide some simple memoization for a fixed range of years
-}
module Utils.Holidays
  ( Feiertagsgebiet(..)  
  , feiertage
  , bankHolidays, bankHolidaysArea, bankHolidaysAreaSet
  , isBankHoliday, isBankHolidayArea 
  , isWeekend
  , addBusinessDays
  ) where

import Import.NoModel

import qualified Data.Set as Set (Set, member)
import qualified Data.Map as Map

--import Data.Time.Calendar.WeekDate
import Data.Time.Calendar.Easter (gregorianEaster)


-- | Some areas / companies within Germany.
-- | The datatype is not yet complete.
data Feiertagsgebiet = Deutschland | Hessen | Bayern | Munich | Fraport 
  deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
  deriving anyclass (Universe, Finite)

-- | List the bank holidays for the given year >= 1995, in ascending order. 
-- | Holidays on a weekend are legally considered holidays in some German states, hence
-- | the behaviour differs from Data.Time.Calendar.BankHoliday.EnglandAndWales by including holidays on Sundays.
-- | Included for compatibility with Data.Time.Calendar.BankHoliday.EnglandAndWales 
bankHolidays :: Integer -> [Day]
bankHolidays = bankHolidaysArea Deutschland

-- | Bank holidays for a region within Germany and a given year >= 1995.
-- | Holidays may occur on a sunday.
-- | For convenience and compatibility.
bankHolidaysArea :: Feiertagsgebiet -> Integer -> [Day]
bankHolidaysArea land year = Map.keys $ feiertage land year 

-- | Bank holidays for a region within Germany and a given year >= 1995.
-- | Holidays may occur on a sunday.
bankHolidaysAreaSet :: Feiertagsgebiet -> Integer -> Set.Set Day
bankHolidaysAreaSet land year = Map.keysSet $ feiertage land year 

-- | Bank holidays for a region within Germany and a given year >= 1995, 
-- | mapped to the german name of each day.
-- | Holidays may occur on a sunday.
feiertage :: Feiertagsgebiet -> Integer -> Map.Map Day String
feiertage land year = case land of     
    Deutschland -> standardHolidays
    Bayern      -> bavarianHolidays
    Munich      ->   munichHolidays
    Hessen      ->  hessianHolidays
    Fraport     ->  fraportHolidays
  where
    easterSunday = gregorianEaster year 
    easterSundayPlus = flip addDays easterSunday

    standardHolidays = Map.fromList
      [ (fromGregorian year  1  1, "Neujahr")      
      , (easterSundayPlus (-2)   , "Karfreitag")
      , (easterSunday            , "Ostersonntag")
      , (easterSundayPlus   1    , "Ostermontag")
      , (fromGregorian year  5  1, "Erster Mai")      
      , (easterSundayPlus 39     , "Himmelfahrt")
      , (easterSundayPlus 49     , "Pfingstsonntag")
      , (easterSundayPlus 50     , "Pfingstmontag")      
      , (fromGregorian year 10  3, "Tag der deutschen Einheit")
      , (fromGregorian year 12 25, "Erster Weihnachtstag")
      , (fromGregorian year 12 26, "Zweiter Weihnachtstag")
      ]

    hessianHolidays = standardHolidays <> map_singleton 
        (easterSundayPlus 60     , "Fronleichnam")      

    bavarianHolidays = hessianHolidays <> Map.fromList
      [ (fromGregorian year  1  6, "Heilige Drei Könige")
      , (fromGregorian year 11  1, "Allerheiligen")
      ]

    munichHolidays = bavarianHolidays <> map_singleton
        (fromGregorian year  8 15, "Maria Himmelfahrt")
    
    fraportHolidays = hessianHolidays <> Map.fromList
      [ (fromGregorian year 12 24, "Heiligabend")
      , (fromGregorian year 12 31, "Sylvester")
      ]

    map_singleton = uncurry Map.singleton

-- | For compatibility with with Data.Time.Calendar.BankHoliday.EnglandAndWales 
-- | only for works for year >= 1995
isBankHoliday :: Day -> Bool
isBankHoliday = isBankHolidayArea Deutschland 

{-- Inefficient, since entire year of holidays is computed for each call 
isBankHolidayArea :: Feiertagsgebiet -> Day -> Bool
isBankHolidayArea land dd = dd `Set.member` holidays
  where 
    (year, _, _) = toGregorian dd
    holidays = bankHolidaysAreaSet land year
-}

-- | Returns whether a day is a bank holiday for years >= 1995
-- | Repeated calls are handled efficiently using lazy memoization 
isBankHolidayArea :: Feiertagsgebiet -> Day -> Bool
isBankHolidayArea land = ibha
  where 
    landHoliday = Map.lookup land memoHolidays 
    ibha dd     = dd `Set.member` holidays
      where 
        (year, _, _) = toGregorian dd
        holidays 
          | (Just hys) <- landHoliday
          , (Just hds) <- index hys $ fromInteger $ year2index year = hds
          | otherwise = bankHolidaysAreaSet land year 

-- memoize holidays 
memoHolidays :: Map.Map Feiertagsgebiet [Set.Set Day]
memoHolidays = Map.fromList  [(land,
                                  [bankHolidaysAreaSet land year
                                  | i <- [0..], let year = index2year i]) 
                              | land <- universeF]    

-- year with fastest access
memoTip :: Integer
memoTip = 2030

year2index :: Integer -> Integer
year2index y 
  | y < memoTip = 2 * (memoTip - y) - 1  
  | otherwise   = 2 * (y - memoTip)

index2year :: Integer -> Integer
index2year y = result
  where 
    (x,r) = y `divMod` 2 
    result | r == 0    = memoTip + x 
           | otherwise = memoTip - x - 1

-- | Test for Saturday/Sunday 
isWeekend :: Day -> Bool
isWeekend = isWeekend' . dayOfWeek 
  where 
    isWeekend' :: WeekDay -> Bool
    isWeekend' Sunday   = True 
    isWeekend' Saturday = True 
    isWeekend' _        = False

-- | Always returns a business day. 
-- | Saturday/Sunday/Holiday treated like next (n>=0) or previous (n<0) working day
addBusinessDays :: Feiertagsgebiet -> Integer -> Day -> Day
addBusinessDays land = abd 
  where 
    ibhal        = isBankHolidayArea land 
    freeday   dd = isWeekend dd || ibhal dd 
    abd n = abd' n
      where         
        (fwd, bwd)  | n >= 0    = (succ, pred)
                    | otherwise = (pred, succ)
        abd' m dd | freeday dd = abd'      m  (fwd dd)              
                  | m == 0     =                   dd
                  | otherwise  = abd' (bwd m) (fwd dd)
