{-|
ASCII ByteString Parsers.
-}
module Attoparsec.Time.ByteString
(
  timeOfDayInISO8601,
  dayInISO8601,
  yearAndMonthInISO8601,
  timeZoneInISO8601,
  utcTimeInISO8601,
  diffTime,
  nominalDiffTime,
)
where

import Attoparsec.Time.Prelude hiding (take, takeWhile)
import Data.Attoparsec.ByteString
import qualified Attoparsec.Time.Pure as A
import qualified Attoparsec.Time.Validation as B
import qualified Data.ByteString as C
import qualified Data.Attoparsec.ByteString.Char8 as D


validated :: Show a => B.Validator a -> Parser a -> Parser a
validated validator parser =
  parser >>= \x -> B.run validator (pure x) fail x

sign :: Parser Bool
sign =
  anyWord8 >>= \case
    43 -> return True
    45 -> return False
    _ -> empty

decimalOfLength :: Integral a => Int -> Parser a
decimalOfLength length =
  do
    bytes <- take length
    if C.all A.word8IsAsciiDigit bytes
      then return (A.decimalFromBytes bytes)
      else fail "Not all chars are valid decimals"

picoWithBasisOfLength :: Int -> Parser Pico
picoWithBasisOfLength basisLength =
  MkFixed <$> ((+) <$> beforePoint <*> ((word8 46 *> afterPoint) <|> pure 0))
  where
    beforePoint =
      (* (10 ^ 12)) <$> decimalOfLength basisLength
    afterPoint =
      fmap (updater . C.take 12) (takeWhile1 A.word8IsAsciiDigit)
      where
        updater bytes =
          let
            afterPoint =
              A.decimalFromBytes bytes
            afterPointLength =
              C.length bytes
            paddedAfterPoint =
              if afterPointLength < 12
                then afterPoint * (10 ^ (12 - afterPointLength))
                else afterPoint
            in paddedAfterPoint

{-# INLINE hour #-}
hour :: Parser Int
hour =
  validated B.hour (decimalOfLength 2) <?> "hour"

{-# INLINE minute #-}
minute :: Parser Int
minute =
  validated B.minute (decimalOfLength 2) <?> "minute"

{-# INLINE second #-}
second :: Parser Pico
second =
  validated B.second (picoWithBasisOfLength 2) <?> "second"

{-|
>>> parseOnly timeOfDayInISO8601 "05:03:58"
Right 05:03:58

>>> parseOnly timeOfDayInISO8601 "05:03:58.02"
Right 05:03:58.02

>>> parseOnly timeOfDayInISO8601 "05:03:58.020"
Right 05:03:58.02

Checks the elements to be within a proper range:

>>> parseOnly timeOfDayInISO8601 "24:00:00"
Left "timeOfDayInISO8601 > hour: Failed reading: Validator \"hour\" failed on the following input: 24"

>>> parseOnly timeOfDayInISO8601 "00:00:60"
Left "timeOfDayInISO8601 > second: Failed reading: Validator \"second\" failed on the following input: 60.000000000000"

Checks the elements to be of proper length:

>>> parseOnly timeOfDayInISO8601 "1:00:00"
Left "timeOfDayInISO8601 > hour: Failed reading: Not all chars are valid decimals"

>>> parseOnly timeOfDayInISO8601 "01:1:00"
Left "timeOfDayInISO8601 > minute: Failed reading: Not all chars are valid decimals"
-}
{-# INLINE timeOfDayInISO8601 #-}
timeOfDayInISO8601 :: Parser TimeOfDay
timeOfDayInISO8601 =
  unnamedParser <?> "timeOfDayInISO8601"
  where
    unnamedParser =
      A.timeOfDay <$>
      (hour <* word8 58) <*>
      (minute <* word8 58) <*>
      (second)

{-|
>>> parseOnly dayInISO8601 "2017-02-01"
Right 2017-02-01

Checks the elements to be in proper range:

>>> parseOnly dayInISO8601 "2017-13-01"
Left "dayInISO8601: Failed reading: Invalid combination of year month and day: (2017,13,1)"

That is accounting for leap year:

>>> parseOnly dayInISO8601 "2017-02-29"
Left "dayInISO8601: Failed reading: Invalid combination of year month and day: (2017,2,29)"

>>> parseOnly dayInISO8601 "2016-02-29"
Right 2016-02-29
-}
{-# INLINE dayInISO8601 #-}
dayInISO8601 :: Parser Day
dayInISO8601 =
  unnamedParser <?> "dayInISO8601"
  where
    unnamedParser =
      do
        year <- decimalOfLength 4
        word8 45
        month <- decimalOfLength 2
        word8 45
        day <- decimalOfLength 2
        case fromGregorianValid year month day of
          Just day -> return day
          Nothing -> fail (error year month day)
      where
        error year month day =
          showString "Invalid combination of year month and day: " $
          show (year, month, day)

{-|
>>> parseOnly yearAndMonthInISO8601 "2016-02"
Right (2016,2)
-}
yearAndMonthInISO8601 :: Parser (Word, Word)
yearAndMonthInISO8601 =
  unnamedParser <?> "yearAndMonthInISO8601"
  where
    unnamedParser =
      do
        year <- decimalOfLength 4
        word8 45
        month <- decimalOfLength 2
        return (year, month)

{-|
>>> parseOnly timeZoneInISO8601 "+01:00"
Right +0100

>>> parseOnly timeZoneInISO8601 "+0100"
Right +0100

>>> parseOnly timeZoneInISO8601 "-0100"
Right -0100

>>> parseOnly timeZoneInISO8601 "Z"
Right UTC
-}
timeZoneInISO8601 :: Parser TimeZone
timeZoneInISO8601 =
  unnamedParser <?> "timeZoneInISO8601"
  where
    unnamedParser =
      z <|> offset
      where
        z =
          word8 90 $> utc
        offset =
          A.timeZone <$> sign <*> decimalOfLength 2 <*> (word8 58 *> decimalOfLength 2 <|> decimalOfLength 2 <|> pure 0)

{-|
>>> parseOnly utcTimeInISO8601 "2017-02-01T05:03:58+01:00"
Right 2017-02-01 04:03:58 UTC
-}
utcTimeInISO8601 :: Parser UTCTime
utcTimeInISO8601 =
  unnamedParser <?> "utcTimeInISO8601"
  where
    unnamedParser =
      do
        day <- dayInISO8601
        word8 84
        time <- timeOfDayInISO8601
        zone <- timeZoneInISO8601
        return (A.utcTimeFromDayAndTimeOfDay day time zone)

{-|
No suffix implies the "seconds" unit:

>>> parseOnly diffTime "10"
Right 10s

Various units (seconds, minutes, hours, days):

>>> parseOnly diffTime "10s"
Right 10s

>>> parseOnly diffTime "10m"
Right 600s

>>> parseOnly diffTime "10h"
Right 36000s

>>> parseOnly diffTime "10d"
Right 864000s

Metric prefixes to seconds (down to Pico):

>>> parseOnly diffTime "10ms"
Right 0.01s

Notice that \"μs\" is not supported, because it's not ASCII.

>>> parseOnly diffTime "10us"
Right 0.00001s

>>> parseOnly diffTime "10ns"
Right 0.00000001s

>>> parseOnly diffTime "10ps"
Right 0.00000000001s

Negative values:

>>> parseOnly diffTime "-1s"
Right -1s

Unsupported units:

>>> parseOnly diffTime "1k"
Left "diffTime: Failed reading: Unsupported unit: \"k\""
-}
diffTime :: Parser DiffTime
diffTime =
  unnamedParser <?> "diffTime"
  where
    unnamedParser =
      do
        amount <- D.scientific
        factor <- timeUnitFactor
        return (factor (realToFrac amount))

{-|
No suffix implies the "seconds" unit:

>>> parseOnly nominalDiffTime "10"
Right 10s

Various units (seconds, minutes, hours, days):

>>> parseOnly nominalDiffTime "10s"
Right 10s

>>> parseOnly nominalDiffTime "10m"
Right 600s

>>> parseOnly nominalDiffTime "10h"
Right 36000s

>>> parseOnly nominalDiffTime "10d"
Right 864000s

Metric prefixes to seconds (down to Pico):

>>> parseOnly nominalDiffTime "10ms"
Right 0.01s

Notice that \"μs\" is not supported, because it's not ASCII.

>>> parseOnly nominalDiffTime "10us"
Right 0.00001s

>>> parseOnly nominalDiffTime "10ns"
Right 0.00000001s

>>> parseOnly nominalDiffTime "10ps"
Right 0.00000000001s

Negative values:

>>> parseOnly nominalDiffTime "-1s"
Right -1s

Unsupported units:

>>> parseOnly nominalDiffTime "1k"
Left "nominalDiffTime: Failed reading: Unsupported unit: \"k\""
-}
nominalDiffTime :: Parser NominalDiffTime
nominalDiffTime =
  unnamedParser <?> "nominalDiffTime"
  where
    unnamedParser =
      do
        amount <- D.scientific
        factor <- timeUnitFactor
        return (factor (realToFrac amount))

timeUnitFactor :: Fractional a => Parser (a -> a)
timeUnitFactor =
  takeWhile A.word8IsAsciiAlpha >>= \case
    "" -> return id
    "s" -> return id
    "ms" -> return (/ 1000)
    "μs" -> return (/ 1000000)
    "us" -> return (/ 1000000)
    "ns" -> return (/ 1000000000)
    "ps" -> return (/ 1000000000000)
    "m" -> return (* 60)
    "h" -> return (* 3600)
    "d" -> return (* 86400)
    unit -> fail ("Unsupported unit: " <> show unit)