{-# LANGUAGE ScopedTypeVariables, PatternGuards #-}
{-# OPTIONS -fno-warn-orphans #-}

-- | A schedule of commands that should be run at a certain time.
module BuildBox.Data.Schedule
        (
        -- * Time Periods
          second, minute, hour, day

        -- * When
        , When          (..)
        , WhenModifier  (..)

        -- * Events
        , EventName
        , Event         (..)
        , earliestEventToStartAt
        , eventCouldStartAt

        -- * Schedules
        , Schedule      (..)
        , makeSchedule
        , lookupEventOfSchedule
        , lookupCommandOfSchedule
        , adjustEventOfSchedule
        , eventsOfSchedule)
where
import Data.Time
import Data.List
import Data.Function
import Data.Maybe
import Control.Monad
import Data.Map                 (Map)
import qualified Data.Map       as Map

instance Read NominalDiffTime where
 readsPrec n str
  = let [(secs :: Double, rest)] = readsPrec n str
    in  case rest of
                's' : rest'     -> [(fromRational $ toRational secs, rest')]
                _               -> []

second, minute, hour, day :: NominalDiffTime
second  = 1
minute  = 60
hour    = 60 * minute
day     = 24 * hour


-- When -------------------------------------------------------------------------------------------
-- | When to invoke some event.
data When
        -- | Just keep doing it.
        = Always

        -- | Don't do it, ever.
        | Never

        -- | Do it some time after we last started it.
        | Every NominalDiffTime

        -- | Do it some time after it last finished.
        | After NominalDiffTime

        -- | Do it each day at this time. The ''days'' are UTC days, not local ones.
        | Daily TimeOfDay
        deriving (Read, Show, Eq)


-- | Modifier to when.
data WhenModifier
        -- | If the event hasn't been invoked before then do it immediately
        --   when we start the cron process.
        = Immediate

        -- | Wait until after this time before doing it first.
        | WaitUntil UTCTime
        deriving (Read, Show, Eq)


-- Event ------------------------------------------------------------------------------------------
type EventName  = String

-- | Records when an event should start, and when it last ran.
data Event
        = Event
        { -- | A unique name for this event.
          --   Used when writing the schedule to a file.
          eventName             :: EventName

          -- | When to run the command.
        , eventWhen             :: When

          -- | Modifier to the previous.
        , eventWhenModifier     :: Maybe WhenModifier

          -- | When the event was last started, if any.
        , eventLastStarted      :: Maybe UTCTime

          -- | When the event last finished, if any.
        , eventLastEnded        :: Maybe UTCTime }
        deriving (Read, Show, Eq)


-- | Given the current time and a list of events, determine which one should be started now.
--   If several events are avaliable then take the one with the earliest start time.
earliestEventToStartAt :: UTCTime -> [Event] -> Maybe Event
earliestEventToStartAt curTime events
 = let  eventsStartable = filter (eventCouldStartAt curTime)   events
        eventsSorted    = sortBy (compare `on` eventLastStarted) eventsStartable
   in   listToMaybe eventsSorted


-- | Given the current time, decide whether an event could be started.
--   If the `WhenModifier` is `Immediate` this always returns true.
--   The `SkipFirst` modifier is ignored, as this is handled separately.
eventCouldStartAt :: UTCTime -> Event -> Bool
eventCouldStartAt curTime event
        -- If the event has never started or ended, and is marked as immediate,
        -- then start it right away.
        | Nothing               <- eventLastStarted  event
        , Nothing               <- eventLastEnded    event
        , Just Immediate        <- eventWhenModifier event
        = True

        -- If the current end time is before the start time, then the most
        -- recent iteration is still running, so don't start it again.
        | Just lastStarted      <- eventLastStarted event
        , Just lastEnded        <- eventLastEnded   event
        , lastEnded < lastStarted
        = False

        -- Keep waiting if there's a seconday wait modifier.
        | Just (WaitUntil waitTime)     <- eventWhenModifier event
        , curTime < waitTime
        = False

        -- Otherwise we have to look at the real schedule.
        | otherwise
        = case eventWhen event of
                Always          -> True
                Never           -> False

                Every diffTime
                 -> maybe True
                        (\lastTime -> (curTime `diffUTCTime` lastTime ) > diffTime)
                        (eventLastStarted event)

                After diffTime
                 -> maybe True
                        (\lastTime -> (curTime `diffUTCTime` lastTime ) > diffTime)
                        (eventLastEnded event)

                Daily timeOfDay
                 -- If it's been less than a day since we last started it, then don't do it yet.
                 | Just lastStarted     <- eventLastStarted event
                 , (curTime `diffUTCTime` lastStarted) < day
                 -> False

                 | otherwise
                 -> let -- If we were going to run it today, this is when it would be.
                        startTimeToday
                                = curTime
                                { utctDayTime   = timeOfDayToTime timeOfDay }

                        -- If it's after that time then quit fooling around..
                    in  curTime > startTimeToday


-- Schedule ---------------------------------------------------------------------------------------
-- | Map of event names to their details and build commands.
data Schedule cmd
        = Schedule (Map EventName (Event, cmd))


-- | Get the list of events in a schedule, ignoring the build commands.
eventsOfSchedule :: Schedule cmd -> [Event]
eventsOfSchedule (Schedule sched)
        = map fst $ Map.elems sched


-- | A nice way to produce a schedule.
makeSchedule :: [(EventName, When, Maybe WhenModifier, cmd)] -> Schedule cmd
makeSchedule tuples
 = let  makeSched (name, whn, mMod, cmd)
          =     (name, (Event name whn mMod Nothing Nothing, cmd))
   in   Schedule $ Map.fromList $ map makeSched tuples


-- | Given an event name, lookup the associated event from a schedule.
lookupEventOfSchedule :: EventName -> Schedule cmd -> Maybe Event
lookupEventOfSchedule name (Schedule sched)
        = liftM fst $ Map.lookup name sched


-- | Given an event name, lookup the associated build command from a schedule.
lookupCommandOfSchedule :: EventName -> Schedule cmd -> Maybe cmd
lookupCommandOfSchedule name (Schedule sched)
        = liftM snd $ Map.lookup name sched


-- | Given a new version of an event, update any matching event in the schedule.
--   If the event not already there then return the original schedule.
adjustEventOfSchedule :: Event -> Schedule cmd -> Schedule cmd
adjustEventOfSchedule event (Schedule sched)
        = Schedule
        $ Map.adjust
                (\(_, build) -> (event, build))
                (eventName event)
                sched