-- | Different logic-related components.

module OrgStat.Helpers
       ( convertRange
       , resolveInputOrg
       , resolveScope
       , resolveReport
       , resolveOutput
       ) where

import           Universum

import           Control.Lens                (at, views, (.=))
import qualified Data.List.NonEmpty          as NE
import           Data.Time                   (LocalTime (..), TimeOfDay (..), addDays,
                                              getZonedTime, toGregorian,
                                              zonedTimeToLocalTime)
import           Data.Time.Calendar          (addGregorianMonthsRollOver)
import           Data.Time.Calendar.WeekDate (toWeekDate)

import           OrgStat.Ast                 (Org (..), cutFromTo, mergeClocks, orgTitle)
import           OrgStat.Config              (ConfDate (..), ConfOutput (..),
                                              ConfRange (..), ConfReport (..),
                                              ConfScope (..), ConfigException (..),
                                              OrgStatConfig (..))
import           OrgStat.IO                  (readOrgFile)
import           OrgStat.Scope               (applyModifiers)
import           OrgStat.WorkMonad           (WorkM, wcConfig, wdReadFiles,
                                              wdResolvedReports, wdResolvedScopes)


-- | 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

-- | Resolves org file: reads from path and puts into state or just
-- gets out of state if was read before.
resolveInputOrg :: FilePath -> WorkM (Text, Org)
resolveInputOrg fp = use (wdReadFiles . at fp) >>= \case
    Just x -> pure x
    Nothing -> do
        todoKeywords <- views wcConfig confTodoKeywords
        o <- readOrgFile todoKeywords fp
        wdReadFiles . at fp .= Just o
        pure o

-- A lot of copy-paste here... 2 bad, though no time to fix

-- | Return scope with requested name or fail. It will be either
-- constructed on the spot or taken from the state if it had been
-- created previously.
resolveScope :: Text -> WorkM Org
resolveScope scopeName = use (wdResolvedScopes . at scopeName) >>= \case
    Just x -> pure x
    Nothing -> constructScope
  where
    constructScope = do
        let filterScopes = filter (\x -> csName x == scopeName)
        views wcConfig (filterScopes . confScopes) >>= \case
            [] ->
                throwM $ ConfigLogicException $
                "Scope "<> scopeName <> " is not declared"
            [sc] -> resolveFoundScope sc
            scopes ->
                throwM $ ConfigLogicException $
                "Multple scopes with name "<> scopeName <>
                " are declared " <> show scopes
    resolveFoundScope ConfScope{..} = do
        orgs <- NE.toList <$> forM csPaths resolveInputOrg
        let orgTop = Org "/" [] [] $ map (\(fn,o) -> o & orgTitle .~ fn) orgs
        wdResolvedScopes . at scopeName .= Just orgTop
        pure orgTop

-- | Same as resolveScope but related to reports.
resolveReport :: Text -> WorkM Org
resolveReport reportName = use (wdResolvedReports . at reportName) >>= \case
    Just x -> pure x
    Nothing -> constructReport
  where
    constructReport = do
        let filterReports = filter (\x -> crName x == reportName)
        views wcConfig (filterReports . confReports) >>= \case
            [] ->
                throwM $ ConfigLogicException $
                "Report " <> reportName <> " is not declared"
            [rep] -> resolveFoundReport rep
            reports ->
                 throwM $ ConfigLogicException $
                "Multple reports with name "<> reportName <>
                " are declared " <> show reports
    resolveFoundReport ConfReport{..} = do
        orgTop <- resolveScope crScope
        fromto <- convertRange crRange
        withModifiers <-
            either throwM pure $
            applyModifiers orgTop crModifiers
        let finalOrg = cutFromTo fromto $ mergeClocks withModifiers
        wdResolvedReports . at reportName .= Just finalOrg
        pure finalOrg

resolveOutput :: Text -> WorkM ConfOutput
resolveOutput outputName =
    views wcConfig (filterOutputs . confOutputs) >>= \case
        [] ->
            throwM $ ConfigLogicException $
            "Report " <> outputName <> " is not declared"
        [rep] -> pure rep
        reports ->
             throwM $ ConfigLogicException $
            "Multple outputs with name "<> outputName <>
            " are declared " <> show reports
  where
    filterOutputs = filter (\x -> coName x == outputName)