{-# LANGUAGE ScopedTypeVariables #-}

-- | Main logic combining all components

module OrgStat.Logic
       ( convertRange
       , runOrgStat
       ) where

import           Data.List                   (notElem, nub, nubBy)
import qualified Data.List.NonEmpty          as NE
import qualified Data.Map                    as M
import qualified Data.Text                   as T
import           Data.Time                   (LocalTime (..), TimeOfDay (..), addDays,
                                              defaultTimeLocale, formatTime, getZonedTime,
                                              toGregorian, zonedTimeToLocalTime)
import           Data.Time.Calendar          (addGregorianMonthsRollOver)
import           Data.Time.Calendar.WeekDate (toWeekDate)
import           System.Directory            (createDirectoryIfMissing)
import           System.FilePath             ((</>))
import           System.Wlog                 (logDebug, logInfo)
import           Universum
import           Unsafe                      (unsafeHead)

import           OrgStat.Ast                 (Org (..), mergeClocks, orgTitle)
import           OrgStat.Config              (ConfDate (..), ConfRange (..),
                                              ConfReport (..), ConfReportType (..),
                                              ConfScope (..), ConfigException (..),
                                              OrgStatConfig (..))
import           OrgStat.IO                  (readConfig, readOrgFile)
import           OrgStat.Report              (processTimeline, tpColorSalt, writeReport)
import           OrgStat.Scope               (applyModifiers)
import           OrgStat.Util                (fromJustM)
import           OrgStat.WorkMonad           (WorkM, wConfigFile, wXdgOpen)
import           Turtle                      (shell)


-- Converts config range to a pair of 'UTCTime', right bound not inclusive.
convertRange :: (MonadIO m) => ConfRange -> m (LocalTime, LocalTime)
convertRange range = case range of
    (ConfFromTo f t)  -> (,) <$> fromConfDate f <*> fromConfDate t
    (ConfBlockDay i) | i < 0 -> error $ "ConfBlockDay i is <0: " <> show i
    (ConfBlockDay 0) -> (,) <$> (localFromDay <$> startOfDay) <*> curTime
    (ConfBlockDay i) -> do
        d <- (negate (i - 1) `addDays`) <$> startOfDay
        pure $ localFromDayPair ((negate 1) `addDays` d, d)
    (ConfBlockWeek i) | i < 0 -> error $ "ConfBlockWeek i is <0: " <> show i
    (ConfBlockWeek 0) -> (,) <$> (localFromDay <$> startOfWeek) <*> curTime
    (ConfBlockWeek i) -> do
        d <- (negate (i - 1) `addWeeks`) <$> startOfWeek
        pure $ localFromDayPair ((negate 1) `addWeeks` d, d)
    (ConfBlockMonth i) | i < 0 -> error $ "ConfBlockMonth i is <0: " <> show i
    (ConfBlockMonth 0) -> (,) <$> (localFromDay <$> startOfMonth) <*> curTime
    (ConfBlockMonth i) -> do
        d <- addGregorianMonthsRollOver (negate $ i-1) <$> startOfMonth
        pure $ localFromDayPair ((negate 1) `addGregorianMonthsRollOver` d, d)
  where
    localFromDay d = LocalTime d $ TimeOfDay 0 0 0
    localFromDayPair = bimap localFromDay localFromDay
    curTime = liftIO $ zonedTimeToLocalTime <$> getZonedTime
    curDay = localDay <$> curTime
    addWeeks i d = (i*7) `addDays` d
    startOfDay = curDay
    startOfWeek = do
        d <- curDay
        let weekDay = pred $ view _3 $ toWeekDate d
        pure $ fromIntegral (negate weekDay) `addDays` d
    startOfMonth = do
        d <- curDay
        let monthDate = pred $ view _3 $ toGregorian d
        pure $ fromIntegral (negate monthDate) `addDays` d
    fromConfDate ConfNow       = curTime
    fromConfDate (ConfLocal x) = pure x


runOrgStat :: WorkM ()
runOrgStat = do
    conf@OrgStatConfig{..} <- readConfig =<< view wConfigFile
    logDebug $ "Config: \n" <> show conf

    curTime <- liftIO getZonedTime
    let reportDir = confOutputDir </> formatTime defaultTimeLocale "%F-%H-%M-%S" curTime
    liftIO $ createDirectoryIfMissing True reportDir
    logInfo $ "This report set will be put into: " <> T.pack reportDir

    logInfo $ "Parsing files..."
    allParsedOrgs <- parseNeededFiles conf
    forM_ confReports $ \ConfReport{..} -> case crType of
        Timeline {..} -> do
            logDebug $ "Processing report " <> crName
            scope <- getScope conf timelineScope crName
            let scopeFiles = NE.toList $ csPaths scope
                neededOrgs =
                    map (\f -> fromMaybe (error $ scopeNotFound (T.pack f) crName) $
                               M.lookup f allParsedOrgs)
                        scopeFiles
            let orgTop = Org "/" [] [] $ map (\(fn,o) -> o & orgTitle .~ fn) neededOrgs
            withModifiers <- mergeClocks <$> applyMods crModifiers orgTop
            let timelineParamsFinal =
                    (confBaseTimelineParams <> timelineParams) & tpColorSalt .~ confColorSalt
            logDebug $ "Launching timeline report with params: " <> show timelineParamsFinal
            fromto <- convertRange timelineRange
            logDebug $ "Using range: " <> show fromto
            res <- processTimeline timelineParamsFinal withModifiers fromto
            logInfo $ "Generating report " <> crName <> "..."
            writeReport reportDir (T.unpack crName) res
    whenM (view wXdgOpen) $ do
        logInfo "Opening reports using xdg-open..."
        void $ shell ("for i in $(ls "<>T.pack reportDir<>"/*); do xdg-open $i; done") empty
  where
    applyMods mods o = case applyModifiers o mods of
        Left k  -> throwM k
        Right r -> pure r
    scopeNotFound scope report =
        mconcat ["Scope ", scope, " is requested for config report ",
                 report, ", but is not present in scopes section"]
    throwLogic = throwM . ConfigLogicException
    getScope OrgStatConfig{..} scopeName reportName =
        fromJustM (throwLogic $ scopeNotFound scopeName reportName) $
        pure $ find ((== scopeName) . csName) confScopes
    getScopes (Timeline _ s _) = [s]
    -- Reads needed org files and returns a map
    parseNeededFiles :: OrgStatConfig -> WorkM (Map FilePath (Text, Org))
    parseNeededFiles conf@OrgStatConfig{..} = do
        let neededScopes =
                nubBy ((==) `on` snd) $
                concatMap (\cr -> map (crName cr,) $ getScopes (crType cr)) confReports
        let availableScopes = map csName confScopes
        let notAvailableScopes = filter (\(_,s) -> s `notElem` availableScopes) neededScopes
        unless (null notAvailableScopes) $ do
            let (r,s) = unsafeHead notAvailableScopes
            throwLogic $ scopeNotFound s r
        neededFiles <-
            nub . concatMap (NE.toList . csPaths) <$>
            mapM (\(r,s) -> getScope conf s r) neededScopes
        fmap M.fromList $ forM neededFiles (\f -> (f,) <$> readOrgFile confTodoKeywords f)