{-# LANGUAGE FlexibleContexts #-}
module Data.Time.HTTP.Common
    ( shortWeekDayName
    , shortWeekDayNameP

    , longWeekDayName
    , longWeekDayNameP

    , shortMonthName
    , shortMonthNameP

    , longMonthName
    , longMonthNameP

    , show2
    , show4

    , read2
    , read4

    , show4digitsTZ
    , read4digitsTZ

    , assertWeekDayIsGood
    , assertGregorianDateIsGood
    , assertTimeOfDayIsGood
    )
    where

import Control.Monad
import Data.Fixed
import Data.Time
import Data.Time.Calendar.WeekDate
import Text.Parsec

shortWeekDayName :: Int -> String
shortWeekDayName 1 = "Mon"
shortWeekDayName 2 = "Tue"
shortWeekDayName 3 = "Wed"
shortWeekDayName 4 = "Thu"
shortWeekDayName 5 = "Fri"
shortWeekDayName 6 = "Sat"
shortWeekDayName 7 = "Sun"
shortWeekDayName n = error ("shortWeekDayName: unknown day number: " ++ show n)

shortWeekDayNameP :: Stream s m Char => ParsecT s u m Int
shortWeekDayNameP
    = choice [ string "Mon" >> return 1
             , char 'T'
               >> choice [ string "ue" >> return 2
                         , string "hu" >> return 4
                         ]
             , string "Wed" >> return 3
             , string "Fri" >> return 5
             , char 'S'
               >> choice [ string "at" >> return 6
                         , string "un" >> return 7
                         ]
             ]

longWeekDayName :: Int -> String
longWeekDayName 1 = "Monday"
longWeekDayName 2 = "Tuesday"
longWeekDayName 3 = "Wednesday"
longWeekDayName 4 = "Thursday"
longWeekDayName 5 = "Friday"
longWeekDayName 6 = "Saturday"
longWeekDayName 7 = "Sunday"

longWeekDayNameP :: Stream s m Char => ParsecT s u m Int
longWeekDayNameP
    = choice [ string "Monday" >> return 1
             , char 'T'
               >> choice [ string "uesday"  >> return 2
                         , string "hursday" >> return 4
                         ]
             , string "Wednesday" >> return 3
             , string "Friday"    >> return 5
             , char 'S'
               >> choice [ string "aturday" >> return 6
                         , string "unday"   >> return 7
                         ]
             ]

shortMonthName :: Int -> String
shortMonthName  1 = "Jan"
shortMonthName  2 = "Feb"
shortMonthName  3 = "Mar"
shortMonthName  4 = "Apr"
shortMonthName  5 = "May"
shortMonthName  6 = "Jun"
shortMonthName  7 = "Jul"
shortMonthName  8 = "Aug"
shortMonthName  9 = "Sep"
shortMonthName 10 = "Oct"
shortMonthName 11 = "Nov"
shortMonthName 12 = "Dec"
shortMonthName  n = error ("shortMonthName: unknown month number: " ++ show n)

shortMonthNameP :: Stream s m Char => ParsecT s u m Int
shortMonthNameP
    = choice [ char 'J'
               >> choice [ string "an" >> return 1
                         , char 'u'
                           >> choice [ char 'n' >> return 6
                                     , char 'l' >> return 7
                                     ]
                         ]
             , string "Feb" >> return 2
             , string "Ma"
               >> choice [ char 'r' >> return 3
                         , char 'y' >> return 5
                         ]
             , char 'A'
               >> choice [ string "pr" >> return 4
                         , string "ug" >> return 8
                         ]
             , string "Sep" >> return 9
             , string "Oct" >> return 10
             , string "Nov" >> return 11
             , string "Dec" >> return 12
             ]

longMonthName :: Int -> String
longMonthName  1 = "January"
longMonthName  2 = "February"
longMonthName  3 = "March"
longMonthName  4 = "April"
longMonthName  5 = "May"
longMonthName  6 = "June"
longMonthName  7 = "July"
longMonthName  8 = "August"
longMonthName  9 = "September"
longMonthName 10 = "October"
longMonthName 11 = "November"
longMonthName 12 = "December"
longMonthName  n = error ("longMonthName: unknown month number: " ++ show n)

longMonthNameP :: Stream s m Char => ParsecT s u m Int
longMonthNameP
    = choice [ char 'J'
               >> choice [ string "anuary" >> return 1
                         , char 'u'
                           >> choice [ string "ne" >> return 6
                                     , string "ly" >> return 7
                                     ]
                         ]
             , string "February" >> return 2
             , string "Ma"
               >> choice [ string "rch" >> return 3
                         , char 'y' >> return 5
                         ]
             , char 'A'
               >> choice [ string "pril" >> return 4
                         , string "ugust" >> return 8
                         ]
             , string "September" >> return 9
             , string "October"   >> return 10
             , string "November"  >> return 11
             , string "December"  >> return 12
             ]

show4 :: Integral i => i -> String
show4 i
    | i >= 0 && i < 10    = "000" ++ show i
    | i >= 0 && i < 100   = "00"  ++ show i
    | i >= 0 && i < 1000  = "0"   ++ show i
    | i >= 0 && i < 10000 = show i
    | otherwise          = error ("show4: the integer i must satisfy 0 <= i < 10000: " ++ show i)

show2 :: Integral i => i -> String
show2 i
    | i >= 0 && i < 10  = "0" ++ show i
    | i >= 0 && i < 100 = show i
    | otherwise         = error ("show2: the integer i must satisfy 0 <= i < 100: " ++ show i)

read4 :: (Stream s m Char, Num n) => ParsecT s u m n
read4 = do n1 <- digit'
           n2 <- digit'
           n3 <- digit'
           n4 <- digit'
           return (n1 * 1000 + n2 * 100 + n3 * 10 + n4)

read2 :: (Stream s m Char, Num n) => ParsecT s u m n
read2 = do n1 <- digit'
           n2 <- digit'
           return (n1 * 10 + n2)

digit' :: (Stream s m Char, Num n) => ParsecT s u m n
digit' = liftM fromC digit

fromC :: Num n => Char -> n
fromC '0' = 0
fromC '1' = 1
fromC '2' = 2
fromC '3' = 3
fromC '4' = 4
fromC '5' = 5
fromC '6' = 6
fromC '7' = 7
fromC '8' = 8
fromC '9' = 9
fromC _   = undefined

show4digitsTZ :: TimeZone -> String
show4digitsTZ tz
    = case timeZoneMinutes tz of
        offset | offset <  0 -> '-' : showTZ' (negate offset)
               | otherwise   -> '+' : showTZ' offset
    where
      showTZ' offset
          = let h = offset `div` 60
                m = offset - h * 60
            in
              concat [show2 h, show2 m]

read4digitsTZ :: Stream s m Char => ParsecT s u m TimeZone
read4digitsTZ
    = do sign   <- (char '+' >> return 1)
                   <|>
                   (char '-' >> return (-1))
         hour   <- read2
         minute <- read2
         let tz = TimeZone {
                    timeZoneMinutes    = (sign * (hour * 60 + minute))
                  , timeZoneSummerOnly = False
                  , timeZoneName       = timeZoneOffsetString tz
                  }
         return tz

assertWeekDayIsGood :: Stream s m t => Int -> Day -> ParsecT s u m ()
assertWeekDayIsGood givenWD gregDay
    = let (_, _, correctWD ) = toWeekDate  gregDay
          (year, month, day) = toGregorian gregDay
      in
        unless (givenWD == correctWD)
                   $ fail
                   $ concat [ "Gregorian day "
                            , show year
                            , "-"
                            , show month
                            , "-"
                            , show day
                            , " is "
                            , longWeekDayName correctWD
                            , ", not "
                            , longWeekDayName givenWD
                            ]

assertGregorianDateIsGood :: Stream s m t => Integer -> Int -> Int -> ParsecT s u m Day
assertGregorianDateIsGood year month day
    = case fromGregorianValid year month day of
        Nothing
            -> fail $ concat [ "Invalid gregorian day: "
                             , show year
                             , "-"
                             , show month
                             , "-"
                             , show day
                             ]
        Just gregDay
            -> return gregDay

assertTimeOfDayIsGood :: Stream s m t => Int -> Int -> Pico -> ParsecT s u m TimeOfDay
assertTimeOfDayIsGood hour minute second
    = case makeTimeOfDayValid hour minute second of
        Nothing
            -> fail $ concat [ "Invalid time of day: "
                             , show hour
                             , ":"
                             , show minute
                             , ":"
                             , showFixed True second
                             ]
        Just tod
            -> return tod