{-# LANGUAGE CPP #-}

module Data.DateTime where

import Data.Fixed (div')
import Data.Function (on)

import Data.Time.Clock hiding (getCurrentTime)
import Data.Time.Format
import Data.Time.LocalTime
import Numeric (fromRat)

#if !MIN_VERSION_time(1,5,0)
import System.Locale
#endif

import System.Time hiding (toClockTime)

import qualified Data.Time.Calendar as Calendar
import qualified Data.Time.Clock as Clock

-- Define a local synonym for UTCTime just to get some insulation from
-- the craziness of the Haskell standard library date and time functions.

type DateTime = UTCTime

-- Defined here so that users don't need to know about Data.Time.Clock.

getCurrentTime :: IO DateTime
getCurrentTime = Clock.getCurrentTime

-- Conversion back and forth between DateTime and MJD.

toMJD :: DateTime -> Rational
toMJD = getModJulianDate . toUniversalTime

toMJD' :: RealFloat a => DateTime -> a
toMJD' = fromRat . toMJD

fromMJD :: Rational -> DateTime
fromMJD = fromUniversalTime . ModJulianDate

fromMJD' :: RealFloat a => a -> DateTime
fromMJD' = fromMJD . realToFrac

-- Because UTCTime is opaque, we need to convert to UniversalTime in
-- order to do anything with it, but these functions are mainly of
-- interest internally.

toUniversalTime :: DateTime -> UniversalTime
toUniversalTime = localTimeToUT1 0 . utcToLocalTime utc

fromUniversalTime :: UniversalTime -> DateTime
fromUniversalTime = localTimeToUTC utc . ut1ToLocalTime 0

-- Take apart a DateTime into pieces and parts.

toGregorian'    :: DateTime -> (Integer, Int, Int)
toGregorian' dt = (y, m, d)
  where
    (y, m, d, _, _, _) = toGregorian dt

toGregorian    :: DateTime -> (Integer, Int, Int, Int, Int, Int)
toGregorian dt = (year, month, day', hours, minutes, seconds `div'` 1)
  where
    LocalTime day tod   = utcToLocalTime utc dt
    (year, month, day') = Calendar.toGregorian day
    TimeOfDay hours minutes seconds = tod

-- Combine pieces and parts to produce a DateTime.

fromGregorian'       :: Integer -> Int -> Int -> DateTime
fromGregorian' y m d = fromGregorian y m d 0 0 0

fromGregorian :: Integer -> Int -> Int -> Int -> Int -> Int -> DateTime
fromGregorian year month day hours minutes seconds =
    UTCTime day' (secondsToDiffTime . fromIntegral $ seconds')
  where
    day'     = Calendar.fromGregorian year month day
    seconds' = 3600 * hours + 60 * minutes + seconds

-- Getting closer to the machine: Not all the functionality of
-- System.Time is available in Data.Time, and the only way we can convert
-- back and forth is to go through seconds.

toSeconds    :: DateTime -> Integer
toSeconds dt = floor $
    (86400.0 :: Double) * fromRational (toMJD dt - startOfTimeMJD)

fromSeconds   :: Integer -> DateTime
fromSeconds s = fromMJD $
    fromIntegral s / 86400 + startOfTimeMJD

toClockTime    :: DateTime -> ClockTime
toClockTime dt = TOD (toSeconds dt) 0

fromClockTime           :: ClockTime -> DateTime
fromClockTime (TOD s _) = fromSeconds s

startOfTime :: DateTime
startOfTime = fromGregorian' 1970 1 1

startOfTimeMJD :: Rational
startOfTimeMJD = toMJD startOfTime

-- Formatting and parsing, with special attention to the format used by
-- ODBC and MySQL.

toSqlString :: DateTime -> String
toSqlString = formatDateTime sqlFormat

fromSqlString :: String -> Maybe DateTime
fromSqlString = parseDateTime sqlFormat

formatDateTime :: String -> DateTime -> String
formatDateTime = formatTime defaultTimeLocale

parseDateTime :: String -> String -> Maybe DateTime
parseDateTime = parseTime defaultTimeLocale

sqlFormat :: String
sqlFormat = iso8601DateFormat (Just "%T")

-- Simple arithmetic.

addMinutes' :: Int -> DateTime -> DateTime
addMinutes' = addMinutes . fromIntegral

addMinutes   :: Integer -> DateTime -> DateTime
addMinutes m = addSeconds (60 * m)

diffMinutes'   :: DateTime -> DateTime -> Int
diffMinutes' x = fromIntegral . diffMinutes x

diffMinutes   :: DateTime -> DateTime -> Integer
diffMinutes x = (`div` 60) . diffSeconds x

addSeconds      :: Integer -> DateTime -> DateTime
addSeconds s dt = fromSeconds $ toSeconds dt + s

diffSeconds :: DateTime -> DateTime -> Integer
diffSeconds = (-) `on` toSeconds