{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
{-# LANGUAGE Safe, NoImplicitPrelude, OverloadedStrings, UnicodeSyntax, TypeFamilies #-}

module Data.Microformats2.Parser.Date where

import           Prelude.Compat
import           Control.Applicative
import           Control.Error.Util (hush)
import           Text.Printf
import           Data.Maybe
import           Data.Foldable
import           Data.Attoparsec.Text
import qualified Data.Time.Calendar as C
import qualified Data.Time.Calendar.OrdinalDate as O
import qualified Data.Text as T

data Date = Date Int Int Int
instance Show Date where
  show (Date y m d) = printf "%d-%02d-%02d" y m d

data HourType = TwentyFourHour | AMHour | PMHour
data Time = Time Int Int Int
instance Show Time where
  show (Time h m s) = printf "%02d:%02d:%02d" h m s

data DateTime = DateTime Date Time
instance Show DateTime where
  show (DateTime d t) = show d ++ "T" ++ show t

data ZoneType = Plus | Minus
data Zone = Zone ZoneType Int Int
instance Show Zone where
  show (Zone Plus  h m) = printf "+%02d:%02d" h m
  show (Zone Minus h m) = printf "-%02d:%02d" h m

data TimeZone = TimeZone Time Zone
instance Show TimeZone where
  show (TimeZone t z) = show t ++ show z

data DateTimeZone = DateTimeZone DateTime Zone
instance Show DateTimeZone where
  show (DateTimeZone dt z) = show dt ++ show z

data DTPart = DatePart Date | TimePart Time | ZonePart Zone | TimeZonePart TimeZone | DateTimePart DateTime | DateTimeZonePart DateTimeZone
instance Show DTPart where
  show (DatePart d) = show d
  show (TimePart t) = show t
  show (ZonePart z) = show z
  show (TimeZonePart tz) = show tz
  show (DateTimePart dt) = show dt
  show (DateTimeZonePart dtz) = show dtz

isDatePart, isTimePart, isZonePart, isTimeZonePart, isDateTimePart, isDateTimeZonePart  DTPart  Bool
isDatePart (DatePart _) = True
isDatePart _ = False
isTimePart (TimePart _) = True
isTimePart _ = False
isZonePart (ZonePart _) = True
isZonePart _ = False
isTimeZonePart (TimeZonePart _) = True
isTimeZonePart _ = False
isDateTimePart (DateTimePart _) = True
isDateTimePart _ = False
isDateTimeZonePart (DateTimeZonePart _) = True
isDateTimeZonePart _ = False

parseDate  Parser Date
parseDate = parseDate'
  where parseDate' = do
          year  read <$> count 4 digit
          char '-'
          parseMMDD year <|> parseDDD year
        parseMMDD year = do
          mm  read <$> count 2 digit
          char '-'
          dd  read <$> count 2 digit
          return $ Date year mm dd
        parseDDD year = do
          ddd  read <$> count 3 digit
          let (_, mm, dd) = C.toGregorian $ O.fromOrdinalDate (fromIntegral year) ddd
          return $ Date year mm dd

parseHourType  Parser HourType
parseHourType =
      ((char 'a' <|> char 'A') >> option '.' (char '.') >> (char 'm' <|> char 'M') >> option '.' (char '.') >> return AMHour)
  <|> ((char 'p' <|> char 'P') >> option '.' (char '.') >> (char 'm' <|> char 'M') >> option '.' (char '.') >> return PMHour)

parseTime  Parser Time
parseTime = do
  hrs   read <$> count 2 digit
  mins  option 0 $ char ':' >> read <$> count 2 digit
  secs  option 0 $ char ':' >> read <$> count 2 digit
  htyp  option TwentyFourHour parseHourType
  let hrs' = case (hrs, htyp) of
               (12, AMHour)  00
               (x,  PMHour) | x < 12  x + 12
               (x,  _)  x
  return $ Time hrs' mins secs

parseZone  Parser Zone
parseZone = (char 'Z' >> return (Zone Plus 0 0)) <|> parseZone'
  where parseZone' = do
          htyp  (char '+' >> return Plus) <|> (char '-' >> return Minus)
          hrs   read <$> count 2 digit
          mins  option 0 $ option ':' (char ':') >> read <$> count 2 digit
          return $ Zone htyp hrs mins

parseTimeZone  Parser TimeZone
parseTimeZone = do
  t  parseTime
  z  parseZone
  return $ TimeZone t z

parseDateTime  Parser DateTime
parseDateTime = do
  d  parseDate
  option 'T' $ char 'T' <|> char ' '
  t  parseTime
  return $ DateTime d t

parseDateTimeZone  Parser DateTimeZone
parseDateTimeZone = do
  dt  parseDateTime
  z  parseZone
  return $ DateTimeZone dt z

parseDTPart  Parser DTPart
parseDTPart =
      (DateTimeZonePart <$> parseDateTimeZone)
  <|> (DateTimePart <$> parseDateTime)
  <|> (DatePart <$> parseDate)
  <|> (TimeZonePart <$> parseTimeZone)
  <|> (TimePart <$> parseTime)
  <|> (ZonePart <$> parseZone)

parseDTParts  (Traversable φ, Monoid (φ DTPart))  φ T.Text  φ DTPart
parseDTParts = fromMaybe mempty . sequence . fmap (hush . parseOnly parseDTPart)

normalizeDTParts  (Foldable φ)  φ DTPart  Maybe DTPart
normalizeDTParts ps = asum [ find isDateTimeZonePart ps, findDateTime, findDateAndTime, find isDatePart ps, find isTimeZonePart ps, find isTimePart ps ]
  where findDateTime = do
          (DateTimePart dt)  find isDateTimePart ps
          return $ case find isZonePart ps of
            Just (ZonePart z)  DateTimeZonePart $ DateTimeZone dt z
            _  DateTimePart dt
        findDateAndTime = do
          (DatePart d)  find isDatePart ps
          case find isTimeZonePart ps of
            Just (TimeZonePart (TimeZone t z))  return $ DateTimeZonePart $ DateTimeZone (DateTime d t) z
            _  findTime d
        findTime d = do
          (TimePart t)  find isTimePart ps
          return $ case find isZonePart ps of
            Just (ZonePart z)  DateTimeZonePart $ DateTimeZone (DateTime d t) z
            _  DateTimePart $ DateTime d t