{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DoAndIfThenElse #-}

module Data.Time.Parsers.Util ( nDigit
                              , isBCE
                              , onlyParse
                              , defaultOptions
                              , withOptions
                              , withDefaultOptions
                              , parseWithOptions
                              , parseWithDefaultOptions
                              , isFlagSet
                              , makeBCE
                              , fromExtendedTimestamp
                              , fromExtendedTimestampIO
                              , module Data.Time.Parsers.Types
                              ) where

import           Data.Time.Parsers.Types

import           Control.Applicative     ((<|>),(<$>),(<*),(*>))
import           Control.Monad.Reader
import           Data.Attoparsec.Char8
import qualified Data.ByteString.Char8   as B
import           Data.Set                as Set (member, fromList)
import           Data.Time

-- | Parse a given number of digits
nDigit :: (Read a, Num a) => Int -> Parser a
nDigit n = read <$> count n digit

-- | Return true if the strings "BC" or "BCE" are consumed, false otherwise
isBCE :: OptionedParser Bool
isBCE = lift . option False $ const True <$> isBCE'
  where
    isBCE' = skipSpace *> (string "BCE" <|> string "BC")

-- | Fail if the given parser fails to consume all of the input
onlyParse :: OptionedParser a -> OptionedParser a
onlyParse p = p <* lift endOfInput

-- | Default Options to use:
-- Try YMD, then MDY, then DMY
-- Accept '.', ' ', '/', '-' as separators.
-- Use flags MakeRecent, DefaultToUTC, DefaultToMidnight
defaultOptions :: Options
defaultOptions = Options { formats = [YMD,MDY,DMY]
                         , seps = ". /-"
                         , flags = Set.fromList [ MakeRecent
                                                , DefaultToUTC
                                                , DefaultToMidnight
                                                ]
                         }

withOptions :: OptionedParser a -> Options -> Parser a
withOptions = runReaderT

withDefaultOptions :: OptionedParser a -> Parser a
withDefaultOptions = flip runReaderT defaultOptions

-- | Use given options and parser to parse a single Timestamp.
-- always feeds empty, so a Partial result is never returned.
-- Ignores preceding and trailing whitespace.
parseWithOptions :: Options -> OptionedParser a ->
                    B.ByteString -> Result a
parseWithOptions opt p = flip feed B.empty . (parse $ runReaderT p' opt)
  where
    p' = onlyParse (lift skipSpace *> p <* lift skipSpace)

-- | Use default options to parse single Timestamp with a given parser,
-- ignoring preceding and trailing whitespace
parseWithDefaultOptions :: OptionedParser a -> B.ByteString -> Result a
parseWithDefaultOptions = parseWithOptions defaultOptions

-- | Return whether a given flag is set.
isFlagSet :: Flag -> OptionedParser Bool
isFlagSet f = asks $ Set.member f . flags

-- | Converts a CE date into a BCE date. Fails if the date is already BCE
-- Warning: If you anticipate BCE dates, it is advisable to not use the
-- MakeRecent flag. It will cause ByteStrings such as "79 BC" to be parsed as
-- "1979 BCE"
makeBCE :: Monad m => Day -> m Day
makeBCE day = let (y,d,m) = toGregorian day
              in  if (y < 0)
                  then fail "Already BCE"
                  else return $ fromGregorian (negate y + 1) d m

-- | Given a timestamp to use as the current time, purely convert an
-- ExtendedTimestamp to a timestamp
fromExtendedTimestamp :: (FromZonedTime a, ToZonedTime a) =>
                         a -> ExtendedTimestamp a -> a
fromExtendedTimestamp now ts = case ts of
    Timestamp a -> a
    Now         -> now
    Yesterday   -> fromZonedTime . addDays' (-1) . atMidnight $ toZonedTime now
    Today       -> fromZonedTime . atMidnight $ toZonedTime now
    Tomorrow    -> fromZonedTime . addDays' 1 . atMidnight $ toZonedTime now
  where
    atMidnight (ZonedTime (LocalTime d _) tz) =
        ZonedTime (LocalTime d midnight) tz
    addDays' n (ZonedTime (LocalTime d tod) tz) =
        ZonedTime (LocalTime (addDays n d) tod) tz

-- | Use getZonedTime to get the current time, and use it to convert an
-- ExtendedTimestamp to a timestamp
fromExtendedTimestampIO :: (FromZonedTime a, ToZonedTime a) =>
                           ExtendedTimestamp a -> IO a
fromExtendedTimestampIO ts = (fromZonedTime <$> getZonedTime) >>=
                             return . flip fromExtendedTimestamp ts