{-# LANGUAGE Safe #-}

{-# OPTIONS -fno-warn-orphans #-}

module Data.Time.Format.Parse.Instances (

) where

import Control.Applicative ((<|>))
import Data.Char
import Data.Fixed
import Data.List (elemIndex, find)
import Data.Ratio
import Data.Time.Calendar.CalendarDiffDays
import Data.Time.Calendar.Days
import Data.Time.Calendar.Gregorian
import Data.Time.Calendar.Month
import Data.Time.Calendar.OrdinalDate
import Data.Time.Calendar.Private (clipValid)
import Data.Time.Calendar.WeekDate
import Data.Time.Clock.Internal.DiffTime
import Data.Time.Clock.Internal.NominalDiffTime
import Data.Time.Clock.Internal.UTCTime
import Data.Time.Clock.Internal.UniversalTime
import Data.Time.Clock.POSIX
import Data.Time.Format.Locale
import Data.Time.Format.Parse.Class
import Data.Time.LocalTime.Internal.CalendarDiffTime
import Data.Time.LocalTime.Internal.LocalTime
import Data.Time.LocalTime.Internal.TimeOfDay
import Data.Time.LocalTime.Internal.TimeZone
import Data.Time.LocalTime.Internal.ZonedTime
import Data.Traversable
import Text.Read (readMaybe)

data DayComponent
    = DCCentury Integer -- century of all years
    | DCCenturyYear Integer -- 0-99, last two digits of both real years and week years
    | DCYearMonth MonthOfYear -- 1-12
    | DCMonthDay DayOfMonth -- 1-31
    | DCYearDay DayOfYear -- 1-366
    | DCWeekDay Int -- 1-7 (mon-sun)
    | DCYearWeek
        WeekType
        WeekOfYear -- 1-53 or 0-53

data WeekType
    = ISOWeek
    | SundayWeek
    | MondayWeek

makeDayComponent :: TimeLocale -> Char -> String -> Maybe [DayComponent]
makeDayComponent l c x =
    let
        ra :: Read a => Maybe a
        ra = readMaybe x
        zeroBasedListIndex :: [String] -> Maybe Int
        zeroBasedListIndex ss = elemIndex (map toUpper x) $ fmap (map toUpper) ss
        oneBasedListIndex :: [String] -> Maybe Int
        oneBasedListIndex ss = do
            index <- zeroBasedListIndex ss
            return $ 1 + index
    in
        case c of
            -- %C: century (all but the last two digits of the year), 00 - 99
            'C' -> do
                a <- ra
                return [DCCentury a]
            -- %f century (all but the last two digits of the year), 00 - 99
            'f' -> do
                a <- ra
                return [DCCentury a]
            -- %Y: year
            'Y' -> do
                a <- ra
                return [DCCentury (a `div` 100), DCCenturyYear (a `mod` 100)]
            -- %G: year for Week Date format
            'G' -> do
                a <- ra
                return [DCCentury (a `div` 100), DCCenturyYear (a `mod` 100)]
            -- %y: last two digits of year, 00 - 99
            'y' -> do
                a <- ra
                return [DCCenturyYear a]
            -- %g: last two digits of year for Week Date format, 00 - 99
            'g' -> do
                a <- ra
                return [DCCenturyYear a]
            -- %B: month name, long form (fst from months locale), January - December
            'B' -> do
                a <- oneBasedListIndex $ fmap fst $ months l
                return [DCYearMonth a]
            -- %b: month name, short form (snd from months locale), Jan - Dec
            'b' -> do
                a <- oneBasedListIndex $ fmap snd $ months l
                return [DCYearMonth a]
            -- %m: month of year, leading 0 as needed, 01 - 12
            'm' -> do
                raw <- ra
                a <- clipValid 1 12 raw
                return [DCYearMonth a]
            -- %d: day of month, leading 0 as needed, 01 - 31
            'd' -> do
                raw <- ra
                a <- clipValid 1 31 raw
                return [DCMonthDay a]
            -- %e: day of month, leading space as needed, 1 - 31
            'e' -> do
                raw <- ra
                a <- clipValid 1 31 raw
                return [DCMonthDay a]
            -- %V: week for Week Date format, 01 - 53
            'V' -> do
                raw <- ra
                a <- clipValid 1 53 raw
                return [DCYearWeek ISOWeek a]
            -- %U: week number of year, where weeks start on Sunday (as sundayStartWeek), 00 - 53
            'U' -> do
                raw <- ra
                a <- clipValid 0 53 raw
                return [DCYearWeek SundayWeek a]
            -- %W: week number of year, where weeks start on Monday (as mondayStartWeek), 00 - 53
            'W' -> do
                raw <- ra
                a <- clipValid 0 53 raw
                return [DCYearWeek MondayWeek a]
            -- %u: day for Week Date format, 1 - 7
            'u' -> do
                raw <- ra
                a <- clipValid 1 7 raw
                return [DCWeekDay a]
            -- %a: day of week, short form (snd from wDays locale), Sun - Sat
            'a' -> do
                a' <- zeroBasedListIndex $ fmap snd $ wDays l
                let
                    a =
                        if a' == 0
                            then 7
                            else a'
                return [DCWeekDay a]
            -- %A: day of week, long form (fst from wDays locale), Sunday - Saturday
            'A' -> do
                a' <- zeroBasedListIndex $ fmap fst $ wDays l
                let
                    a =
                        if a' == 0
                            then 7
                            else a'
                return [DCWeekDay a]
            -- %w: day of week number, 0 (= Sunday) - 6 (= Saturday)
            'w' -> do
                raw <- ra
                a' <- clipValid 0 6 raw
                let
                    a =
                        if a' == 0
                            then 7
                            else a'
                return [DCWeekDay a]
            -- %j: day of year for Ordinal Date format, 001 - 366
            'j' -> do
                raw <- ra
                a <- clipValid 1 366 raw
                return [DCYearDay a]
            -- unrecognised, pass on to other parsers
            _ -> return []

makeDayComponents :: TimeLocale -> [(Char, String)] -> Maybe [DayComponent]
makeDayComponents l pairs = do
    components <- for pairs $ \(c, x) -> makeDayComponent l c x
    return $ concat components

safeLast :: a -> [a] -> a
safeLast x xs = last (x : xs)

instance ParseTime Day where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l pairs = do
        cs <- makeDayComponents l pairs
        -- 'Nothing' indicates a parse failure,
        -- while 'Just []' means no information
        let
            y =
                let
                    d = safeLast 70 [x | DCCenturyYear x <- cs]
                    c =
                        safeLast
                            ( if d >= 69
                                then 19
                                else 20
                            )
                            [x | DCCentury x <- cs]
                in
                    100 * c + d
            rest (DCYearMonth m : _) =
                let
                    d = safeLast 1 [x | DCMonthDay x <- cs]
                in
                    fromGregorianValid y m d
            rest (DCYearDay d : _) = fromOrdinalDateValid y d
            rest (DCYearWeek wt w : _) =
                let
                    d = safeLast 4 [x | DCWeekDay x <- cs]
                in
                    case wt of
                        ISOWeek -> fromWeekDateValid y w d
                        SundayWeek -> fromSundayStartWeekValid y w (d `mod` 7)
                        MondayWeek -> fromMondayStartWeekValid y w d
            rest (_ : xs) = rest xs
            rest [] = rest [DCYearMonth 1]
        rest cs

instance ParseTime Month where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l pairs = do
        cs <- makeDayComponents l pairs
        -- 'Nothing' indicates a parse failure,
        -- while 'Just []' means no information
        let
            y =
                let
                    d = safeLast 70 [x | DCCenturyYear x <- cs]
                    c =
                        safeLast
                            ( if d >= 69
                                then 19
                                else 20
                            )
                            [x | DCCentury x <- cs]
                in
                    100 * c + d
            rest (DCYearMonth m : _) = fromYearMonthValid y m
            rest (_ : xs) = rest xs
            rest [] = fromYearMonthValid y 1
        rest cs

mfoldl :: Monad m => (a -> b -> m a) -> m a -> [b] -> m a
mfoldl f =
    let
        mf ma b = do
            a <- ma
            f a b
    in
        foldl mf

instance ParseTime TimeOfDay where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l =
        let
            f t@(TimeOfDay h m s) (c, x) =
                let
                    ra :: Read a => Maybe a
                    ra = readMaybe x
                    getAmPm =
                        let
                            upx = map toUpper x
                            (amStr, pmStr) = amPm l
                        in
                            if upx == amStr
                                then Just $ TimeOfDay (h `mod` 12) m s
                                else
                                    if upx == pmStr
                                        then
                                            Just $
                                                TimeOfDay
                                                    ( if h < 12
                                                        then h + 12
                                                        else h
                                                    )
                                                    m
                                                    s
                                        else Nothing
                in
                    case c of
                        'P' -> getAmPm
                        'p' -> getAmPm
                        'H' -> do
                            raw <- ra
                            a <- clipValid 0 23 raw
                            return $ TimeOfDay a m s
                        'I' -> do
                            raw <- ra
                            a <- clipValid 1 12 raw
                            return $ TimeOfDay a m s
                        'k' -> do
                            raw <- ra
                            a <- clipValid 0 23 raw
                            return $ TimeOfDay a m s
                        'l' -> do
                            raw <- ra
                            a <- clipValid 1 12 raw
                            return $ TimeOfDay a m s
                        'M' -> do
                            raw <- ra
                            a <- clipValid 0 59 raw
                            return $ TimeOfDay h a s
                        'S' -> do
                            raw <- ra
                            a <- clipValid 0 60 raw
                            return $ TimeOfDay h m (fromInteger a)
                        'q' -> do
                            ps <- (readMaybe $ take 12 $ rpad 12 '0' x) <|> return 0
                            return $ TimeOfDay h m (mkPico (floor s) ps)
                        'Q' ->
                            if null x
                                then Just t
                                else do
                                    ps <- (readMaybe $ take 12 $ rpad 12 '0' x) <|> return 0
                                    return $ TimeOfDay h m (mkPico (floor s) ps)
                        _ -> Just t
        in
            mfoldl f (Just midnight)

rpad :: Int -> a -> [a] -> [a]
rpad n c xs = xs ++ replicate (n - length xs) c

mkPico :: Integer -> Integer -> Pico
mkPico i f = fromInteger i + fromRational (f % 1000000000000)

instance ParseTime LocalTime where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l xs = LocalTime <$> (buildTime l xs) <*> (buildTime l xs)

enumDiff :: Enum a => a -> a -> Int
enumDiff a b = (fromEnum a) - (fromEnum b)

getMilZoneHours :: Char -> Maybe Int
getMilZoneHours c
    | c < 'A' = Nothing
getMilZoneHours c
    | c <= 'I' = Just $ 1 + enumDiff c 'A'
getMilZoneHours 'J' = Nothing
getMilZoneHours c
    | c <= 'M' = Just $ 10 + enumDiff c 'K'
getMilZoneHours c
    | c <= 'Y' = Just $ (enumDiff 'N' c) - 1
getMilZoneHours 'Z' = Just 0
getMilZoneHours _ = Nothing

getMilZone :: Char -> Maybe TimeZone
getMilZone c =
    let
        yc = toUpper c
    in
        do
            hours <- getMilZoneHours yc
            return $ TimeZone (hours * 60) False [yc]

getKnownTimeZone :: TimeLocale -> String -> Maybe TimeZone
getKnownTimeZone locale x = find (\tz -> map toUpper x == timeZoneName tz) (knownTimeZones locale)

instance ParseTime TimeZone where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l =
        let
            f :: Char -> String -> TimeZone -> Maybe TimeZone
            f 'z' str (TimeZone _ dst name)
                | Just offset <- readTzOffset str = Just $ TimeZone offset dst name
            f 'z' _ _ = Nothing
            f 'Z' str _
                | Just offset <- readTzOffset str = Just $ TimeZone offset False ""
            f 'Z' str _
                | Just zone <- getKnownTimeZone l str = Just zone
            f 'Z' "UTC" _ = Just utc
            f 'Z' [c] _
                | Just zone <- getMilZone c = Just zone
            f 'Z' _ _ = Nothing
            f _ _ tz = Just tz
        in
            foldl (\mt (c, s) -> mt >>= f c s) (Just $ minutesToTimeZone 0)

readTzOffset :: String -> Maybe Int
readTzOffset str =
    let
        getSign '+' = Just 1
        getSign '-' = Just (-1)
        getSign _ = Nothing
        calc s h1 h2 m1 m2 = do
            sign <- getSign s
            h <- readMaybe [h1, h2]
            m <- readMaybe [m1, m2]
            return $ sign * (60 * h + m)
    in
        case str of
            (s : h1 : h2 : ':' : m1 : m2 : []) -> calc s h1 h2 m1 m2
            (s : h1 : h2 : m1 : m2 : []) -> calc s h1 h2 m1 m2
            _ -> Nothing

instance ParseTime ZonedTime where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l xs =
        let
            f (ZonedTime (LocalTime _ tod) z) ('s', x) = do
                a <- readMaybe x
                let
                    s = fromInteger a
                    (_, ps) = properFraction (todSec tod) :: (Integer, Pico)
                    s' = s + fromRational (toRational ps)
                return $ utcToZonedTime z (posixSecondsToUTCTime s')
            f t _ = Just t
        in
            mfoldl f (ZonedTime <$> (buildTime l xs) <*> (buildTime l xs)) xs

instance ParseTime UTCTime where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l xs = zonedTimeToUTC <$> buildTime l xs

instance ParseTime UniversalTime where
    substituteTimeSpecifier _ = timeSubstituteTimeSpecifier
    parseTimeSpecifier _ = timeParseTimeSpecifier
    buildTime l xs = localTimeToUT1 0 <$> buildTime l xs

buildTimeMonths :: [(Char, String)] -> Maybe Integer
buildTimeMonths xs = do
    tt <-
        for xs $ \(c, s) ->
            case c of
                'y' -> fmap ((*) 12) $ readMaybe s
                'b' -> readMaybe s
                'B' -> readMaybe s
                _ -> return 0
    return $ sum tt

buildTimeDays :: [(Char, String)] -> Maybe Integer
buildTimeDays xs = do
    tt <-
        for xs $ \(c, s) ->
            case c of
                'w' -> fmap ((*) 7) $ readMaybe s
                'd' -> readMaybe s
                'D' -> readMaybe s
                _ -> return 0
    return $ sum tt

buildTimeSeconds :: [(Char, String)] -> Maybe Pico
buildTimeSeconds xs = do
    tt <- for xs $ \(c, s) ->
        let
            readInt :: Integer -> Maybe Pico
            readInt t = do
                i <- readMaybe s
                return $ fromInteger $ i * t
        in
            case c of
                'h' -> readInt 3600
                'H' -> readInt 3600
                'm' -> readInt 60
                'M' -> readInt 60
                's' -> readMaybe s
                'S' -> readMaybe s
                _ -> return 0
    return $ sum tt

instance ParseTime NominalDiffTime where
    parseTimeSpecifier _ = durationParseTimeSpecifier
    buildTime _ xs = do
        dd <- buildTimeDays xs
        tt <- buildTimeSeconds xs
        return $ (fromInteger dd * 86400) + realToFrac tt

instance ParseTime DiffTime where
    parseTimeSpecifier _ = durationParseTimeSpecifier
    buildTime _ xs = do
        dd <- buildTimeDays xs
        tt <- buildTimeSeconds xs
        return $ (fromInteger dd * 86400) + realToFrac tt

instance ParseTime CalendarDiffDays where
    parseTimeSpecifier _ = durationParseTimeSpecifier
    buildTime _ xs = do
        mm <- buildTimeMonths xs
        dd <- buildTimeDays xs
        return $ CalendarDiffDays mm dd

instance ParseTime CalendarDiffTime where
    parseTimeSpecifier _ = durationParseTimeSpecifier
    buildTime locale xs = do
        mm <- buildTimeMonths xs
        tt <- buildTime locale xs
        return $ CalendarDiffTime mm tt
