module Codec.MIME.String.Date
      (FullDate(FullDate), DOW(..), Date(Date), Day, Month(..), Year,
       Time(Time), Zone, TimeOfDay(TimeOfDay), Hour, Minute, Second,
       show_full_date, show_mbox_full_date, get_date, p_date_time,
       get_current_date, epochDate,
      )
      where

import Prelude hiding ( (<*>), (<$>), (<*), (<$) )
import Codec.MIME.String.Internal.ABNF
      (
       Parser, parse,
       (<$>), (<$), (<*>), (<*), (<|>),
       pEOI, pPred, pChar, pAtLeast, pFromTo, pExactly, pMaybe,
      )
import Codec.MIME.String.Headers (cws, p_ci_string)
import Codec.MIME.String.Internal.Utils

import Control.Monad.Trans (MonadIO, liftIO)
import System.Time hiding (Month(May), Day)
import qualified System.Time as Time (Month(May))

data FullDate = FullDate (Maybe DOW) Date Time
    deriving (Show, Read)

data DOW = Mon | Tue | Wed | Thu | Fri | Sat | Sun
    deriving (Show, Read)

data Date = Date Day Month Year
    deriving (Show, Read)
type Day = Int
data Month = Jan | Feb | Mar | Apr | May | Jun
           | Jul | Aug | Sep | Oct | Nov | Dec
    deriving (Show, Read)
type Year = Int

data Time = Time TimeOfDay Zone
    deriving (Show, Read)
type Zone = Int
data TimeOfDay = TimeOfDay Hour Minute (Maybe Second)
    deriving (Show, Read)
type Hour = Int
type Minute = Int
type Second = Int

epochDate :: FullDate
epochDate = FullDate (Just Thu) (Date 01 Jan 1970)
                     (Time (TimeOfDay 0 0 (Just 0)) 0)

------------------------ Showing

show_full_date :: FullDate -> String
show_full_date (FullDate m_dow date time)
 = shown_dow ++ show_date date ++ " " ++ show_time time
    where shown_dow = case m_dow of
                          Nothing -> ""
                          Just dow -> show dow ++ ", "

show_date :: Date -> String
show_date (Date day month year)
  = show_int 2 day ++ " " ++ show month ++ " " ++ show year

show_time :: Time -> String
show_time (Time tod zone) = show_tod tod ++ " " ++ show_zone zone

show_tod :: TimeOfDay -> String
show_tod (TimeOfDay h m m_s)
 = show_int 2 h ++ ":" ++ show_int 2 m ++ shown_s
    where shown_s = case m_s of
                        Nothing -> ""
                        Just s -> ":" ++ show_int 2 s

show_zone :: Zone -> String
show_zone z = (if z < 0 then '-' else '+'):show_int 4 (abs z)

show_int :: Int -> Int -> String
show_int digits int = let s = show int
                      in replicate (digits - length s) '0' ++ s

-- Showing for the "From " line in mboxes is sadly a slightly different format

show_mbox_full_date :: FullDate -> String
show_mbox_full_date (FullDate m_dow (Date day month year) (Time tod _))
 = shown_dow ++ show month ++ " " ++ show_int 2 day ++ " " ++
   show_tod tod ++ " " ++ show year
    where shown_dow = case m_dow of
                          Nothing -> ""
                          Just dow -> show dow ++ " "

------------------------ Parsing

get_date :: String -> Maybe FullDate
get_date xs
 = case parse ph_date xs of
       Left f -> Just f
       Right _ -> Nothing

ph_date :: Parser Char FullDate
ph_date = id
      <$  cws
      <*> p_date_time
      <*  cws
      <*  pEOI

p_date_time :: Parser Char FullDate
p_date_time = FullDate
          <$> pMaybe (    id
                      <$> p_dow
                      <*  cws
                      <*  pChar ','
                      <*  cws)
          <*> p_date
          <*  cws
          <*> p_time

p_dow :: Parser Char DOW
p_dow = Mon <$  p_ci_string "Mon"
    <|> Tue <$  p_ci_string "Tue"
    <|> Wed <$  p_ci_string "Wed"
    <|> Thu <$  p_ci_string "Thu"
    <|> Fri <$  p_ci_string "Fri"
    <|> Sat <$  p_ci_string "Sat"
    <|> Sun <$  p_ci_string "Sun"

p_date :: Parser Char Date
p_date = Date
     <$> p_day
     <*  cws
     <*> p_month
     <*  cws
     <*> p_year

-- obs-year merged in
p_year :: Parser Char Year
p_year = (\ds -> let y = read ds
                 in case ds of
                        [_, _, _] -> 2000 + y
                        [_, _]
                         | y < 50 -> 1900 + y
                         | otherwise -> 2000 + y
                        _ -> y)
     <$> pAtLeast 2 (pPred isAsciiDigit)

p_month :: Parser Char Month
p_month = Jan <$  p_ci_string "Jan"
      <|> Feb <$  p_ci_string "Feb"
      <|> Mar <$  p_ci_string "Mar"
      <|> Apr <$  p_ci_string "Apr"
      <|> May <$  p_ci_string "May"
      <|> Jun <$  p_ci_string "Jun"
      <|> Jul <$  p_ci_string "Jul"
      <|> Aug <$  p_ci_string "Aug"
      <|> Sep <$  p_ci_string "Sep"
      <|> Oct <$  p_ci_string "Oct"
      <|> Nov <$  p_ci_string "Nov"
      <|> Dec <$  p_ci_string "Dec"

p_day :: Parser Char Day
p_day = read <$> pFromTo 1 2 (pPred isAsciiDigit)

p_time :: Parser Char Time
p_time = Time
     <$> p_time_of_day
     <*  cws
     <*> p_zone

p_time_of_day :: Parser Char TimeOfDay
p_time_of_day = TimeOfDay
            <$> p_hour
            <*  cws
            <*  pChar ':'
            <*  cws
            <*> p_minute
            <*> pMaybe (    id
                        <$  cws
                        <*  pChar ':'
                        <*  cws
                        <*> p_second)

p_hour :: Parser Char Hour
p_hour = read <$> pExactly 2 (pPred isAsciiDigit)

p_minute :: Parser Char Minute
p_minute = read <$> pExactly 2 (pPred isAsciiDigit)

p_second :: Parser Char Second
p_second = read <$> pExactly 2 (pPred isAsciiDigit)

p_zone :: Parser Char Zone
p_zone = (\f n -> f $ read n)
     <$> (id <$  pChar '+' <|> negate <$  pChar '-')
     <*> pExactly 4 (pPred isAsciiDigit)
 <|> p_obs_zone

p_obs_zone :: Parser Char Zone
p_obs_zone = 0 <$  p_ci_string "UT"
         <|> 0 <$  p_ci_string "GMT"
         <|> -500 <$  p_ci_string "EST"
         <|> -400 <$  p_ci_string "EDT"
         <|> -600 <$  p_ci_string "CST"
         <|> -500 <$  p_ci_string "CDT"
         <|> -700 <$  p_ci_string "MST"
         <|> -600 <$  p_ci_string "MDT"
         <|> -800 <$  p_ci_string "PST"
         <|> -700 <$  p_ci_string "PDT"
         -- Military time zones. Strictly we shouldn't accept [jJ]
         -- but no harm done.
         -- 'they SHOULD all be considered equivalent to "-0000"' as
         -- RFC 822 defined them incorrectly.
         <|> 0 <$  pPred isAsciiAlpha

get_current_date :: MonadIO m => m FullDate
get_current_date
 = do clt <- liftIO getClockTime
      cat <- liftIO $ toCalendarTime clt
      let fd = FullDate (Just dow) date time
          get_dow Sunday    = Sun
          get_dow Monday    = Mon
          get_dow Tuesday   = Tue
          get_dow Wednesday = Wed
          get_dow Thursday  = Thu
          get_dow Friday    = Fri
          get_dow Saturday  = Sat
          get_month January   = Jan
          get_month February  = Feb
          get_month March     = Mar
          get_month April     = Apr
          get_month Time.May  = May
          get_month June      = Jun
          get_month July      = Jul
          get_month August    = Aug
          get_month September = Sep
          get_month October   = Oct
          get_month November  = Nov
          get_month December  = Dec
          dow = get_dow (ctWDay cat)
          date = Date (ctDay cat) (get_month (ctMonth cat)) (ctYear cat)
          time = Time tod (ctTZ cat `div` 36)
          tod = TimeOfDay (ctHour cat) (ctMin cat) (Just (ctSec cat))
      return fd