{-
ToDo:
only read log file if present
translation from country code to country name
   including warning for unknown countries
-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Main where

import qualified Control.Monad.Trans.State as SM
import qualified Control.Monad.Trans.Reader as RM
import Control.Monad.Trans (MonadIO, liftIO, lift, )

import Data.Tuple.HT (swap, mapSnd, )
import Data.List.HT (mapAdjacent, )
import Data.Maybe.HT (toMaybe, )
import Data.Maybe (mapMaybe, )
import Data.Char (isAlpha, toUpper, )
import Data.List (sort, mapAccumL, )
import qualified Data.List.Key as Key

import Data.Time.LocalTime (timeToTimeOfDay, timeOfDayToTime, )
import Data.Time.Format (readTime, )
import System.Locale (defaultTimeLocale, )
import qualified Data.Time.Format as TimeFmt
import qualified Data.Time.Clock as Clock

import qualified Data.Map as Map
import Data.Map (Map)

import qualified System.IO.Strict as SIO
import qualified System.IO as IO


type Country = String
data Contestant =
   Contestant {
      contestantCountry :: Country,
      contestantNumber :: Int
   }
   deriving (Eq, Show)
type Time = Clock.DiffTime

type Entries = Map Country (Int, Time)


newtype M a = M (SM.StateT Entries (RM.ReaderT IO.Handle IO) a)
   deriving (Functor, Monad, MonadIO)

gets :: (Entries -> a) -> M a
gets f =
   M (SM.gets f)

modify :: (Entries -> Entries) -> M ()
modify f =
   M (SM.modify f)


logFileName :: FilePath
logFileName = "toilet.log"


parseContestant :: String -> Either Country Contestant
parseContestant str =
   let (country, number) = span isAlpha str
   in  case reads number of
         [(n, "")] -> Right $ Contestant country n
         _ -> Left country

output :: String -> M ()
output str =
   liftIO $ putStrLn $ "       " ++ str

data Direction = Enter | Leave
   deriving (Eq, Show, Enum)

writeLog :: Contestant -> Time -> Direction -> M ()
writeLog cnt time dir =
   M (lift RM.ask >>= \h ->
      liftIO $
        (IO.hPutStrLn h $
         formatContestant cnt ++ " " ++
         formatDirection dir ++ " " ++
         formatTime time) >>
        IO.hFlush h)

logToEntries :: String -> Entries
logToEntries =
   foldl (\ent (Contestant country number, dirStr, time) ->
      case dirStr of
         Enter -> Map.insert country (number, time) ent
         Leave -> Map.delete country ent)
      Map.empty .
   parseLog

parseLog :: String -> [(Contestant, Direction, Time)]
parseLog =
   map (\[cntStr,dirStr,timeStr] ->
      (either
         (error $ "invalid contestant identifier " ++ cntStr)
         id $ parseContestant cntStr,
       case dirStr of
          "enters" -> Enter
          "leaves" -> Leave
          _ -> error $ "invalid direction " ++ dirStr,
       parseTime timeStr)) .
   map words .
   lines

timeZoneOffset :: Time
timeZoneOffset = 2*60*60

formatDirection :: Direction -> String
formatDirection dir =
   case dir of
      Enter -> "enters"
      Leave -> "leaves"

formatContestant :: Contestant -> String
formatContestant (Contestant country number) =
   country ++ show number

parseTime :: String -> Time
parseTime =
   subtract timeZoneOffset .
   timeOfDayToTime .
   readTime defaultTimeLocale "%R"

formatTime :: Time -> String
formatTime =
   TimeFmt.formatTime defaultTimeLocale "%R" .
   timeToTimeOfDay .
   (timeZoneOffset+)

blockedMsg :: Contestant -> Time -> M ()
blockedMsg cnt time =
   output $
      "toilet blocked by " ++ formatContestant cnt ++
      " since " ++ formatTime time

caseToilet ::
   Country ->
   (Int -> Time -> M a) ->
   (M a) ->
   M a
caseToilet country ifBlocked ifFree =
   maybe
      ifFree
      (\(number, time) -> ifBlocked number time)
     =<< gets (Map.lookup country)

listToilet :: M ()
listToilet =
   mapM_ (\(country, (number, time)) ->
      output $ country ++ show number ++ " since " ++ formatTime time) =<<
   gets Map.toAscList

countryFrequencies :: String -> [(Int, String)]
countryFrequencies =
   reverse . sort .
   map swap .
   Map.toList .
   Map.fromListWith (+) .
   map (\(cnt,_,_) -> (contestantCountry cnt, 1)) .
   filter (\(_,dir,_) -> dir == Enter) .
   parseLog


type Minute = Int

countryTotalTimes :: String -> [(Minute, String)]
countryTotalTimes =
   reverse . sort .
   map swap .
   Map.toList .
   Map.fromListWith (+) .
   map (\(cnt,dir,time) ->
      (contestantCountry cnt,
       minutesFromTime $
       case dir of
          Enter -> -time
          Leave -> time)) .
   parseLog


countryMaximumTimes :: String -> [(Minute, String)]
countryMaximumTimes =
   reverse . sort .
   map swap .
   Map.toList .
   accumulateDurations max .
   map (\(cnt,dir,time) ->
      (contestantCountry cnt,
       dir,
       minutesFromTime time)) .
   parseLog

accumulateDurations ::
   (Num time, Ord key) =>
   (time -> time -> time) ->
   [(key, Direction, time)] -> Map key time
accumulateDurations acc =
   snd .
   foldl
      (\(startTimes, maxTimes) (cnt,dir,time) ->
          case dir of
             Enter ->
                (Map.insertWith (error "entered twice") cnt time startTimes,
                 maxTimes)
             Leave ->
                (Map.delete cnt startTimes,
                 let duration =
                        time -
                        Map.findWithDefault (error "never entered") cnt startTimes
                 in  Map.insertWith acc cnt duration maxTimes))
      (Map.empty, Map.empty)

minutesFromTime :: Time -> Int
minutesFromTime =
   round . (/ (60::Double)) . realToFrac

{- |
Contestants that left toilet immediately
are probably actually corrections by the registrars.
-}
contestantImmediateLeave :: String -> [(Contestant, Time)]
contestantImmediateLeave =
   mapMaybe
      (\((cnt0,dir0,time0), (cnt1,dir1,time1)) ->
         toMaybe
            (cnt0==cnt1 && dir0==Enter && dir1==Leave && time1-time0 <= 60)
            (cnt0, time0)) .
   mapAdjacent (,) .
   parseLog


loadPerTime :: String -> [(Time, Int)]
loadPerTime =
   snd .
   mapAccumL
      (\oldLoad (time,loadDiff) ->
         let newLoad = oldLoad + loadDiff
         in  (newLoad, (time, newLoad))) 0 .
   map (mapSnd $
      sum . map (\dir -> case dir of Enter -> 1; Leave -> -1)) .
   buckets (take (4*60+30) $ iterate (60+) (parseTime "09:00")) .
   map (\(_cnt,dir,time) -> (time, dir)) .
   parseLog


buckets :: (Eq a) =>
   [a] -> [(a,b)] -> [(a,[b])]
buckets as bs =
   snd $
   mapAccumL
      (\bs0 a ->
         let (bucket, bs1) = span ((a==) . fst) bs0
         in  (bs1, (a, map snd bucket)))
      bs as


generateStatistics :: IO ()
generateStatistics =
   do str <- readFile "toilet-correct.log"
      writeFile "country-frequency.csv" . unlines .
         map (\(n,country) -> show n ++ " " ++ country) . countryFrequencies $ str
      writeFile "country-totaltimes.csv" . unlines .
         map (\(n,country) -> show n ++ " " ++ country) . countryTotalTimes $ str
      writeFile "country-maxtimes.csv" . unlines .
         map (\(n,country) -> show n ++ " " ++ country) . countryMaximumTimes $ str
      writeFile "time-load.csv" . unlines .
         map (\(time,load) -> formatTime time ++ " " ++ show load) . loadPerTime $ str


{-
writeFile "toilet-by-country.log" . sortForCountry =<< readFile "toilet-correct.log"
-}
sortForCountry :: String -> String
sortForCountry =
   unlines .
   map unlines .
   Key.group (take 3) .
   Key.sort (take 3) .
   lines


{-
writeFile "toilet-by-contestant.log" . sortForContestant =<< readFile "toilet-correct.log"
-}
sortForContestant :: String -> String
sortForContestant =
   unlines .
   map unlines .
   Key.group (take 4) .
   Key.sort (take 4) .
   lines


loop :: M ()
loop =
   do ln <- fmap (map toUpper) $ liftIO getLine
      if ln=="LIST"
        then listToilet
        else
          case parseContestant ln of
             Left country ->
                caseToilet country
                   (blockedMsg . Contestant country)
                   (output "toilet free")
             Right cnt@(Contestant country number) ->
                fmap Clock.utctDayTime (liftIO Clock.getCurrentTime) >>= \time ->
                caseToilet country
                   (\oldNumber oldTime ->
                       let oldCnt = Contestant country oldNumber
                       in  if number==oldNumber
                             then
                                output (ln ++ " leaves toilet at " ++ formatTime time) >>
                                writeLog oldCnt time Leave >>
                                modify (Map.delete country)
                             else blockedMsg oldCnt oldTime)
                   (do writeLog cnt time Enter
                       modify (Map.insert country (number, time))
                       output (ln ++ " enters toilet at " ++ formatTime time))
      loop

main :: IO ()
main =
   let M lp = loop
   in  SIO.readFile logFileName >>= \logContent ->
       IO.withFile logFileName IO.AppendMode $ \h ->
       flip RM.runReaderT h $ flip SM.evalStateT
          (logToEntries logContent) lp
