-- |
-- Module      : Data.Hourglass.Internal.Unix
-- License     : BSD-style
-- Maintainer  : Vincent Hanquez <vincent@snarc.org>
-- Stability   : experimental
-- Portability : unknown
--
-- Time lowlevel helpers for the unix operating system
--
-- depend on localtime_r and gmtime_r.
-- Some obscure unix system might not support them.
--
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE EmptyDataDecls #-}
module Data.Hourglass.Internal.Unix
    ( dateTimeFromUnixEpochP
    , dateTimeFromUnixEpoch
    , systemGetTimezone
    , systemGetElapsed
    , systemGetElapsedP
    ) where

import Control.Applicative
import Foreign.C.Types
import Foreign.Storable
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Data.Hourglass.Types
import System.IO.Unsafe

-- | convert a unix epoch precise to DateTime
dateTimeFromUnixEpochP :: ElapsedP -> DateTime
dateTimeFromUnixEpochP (ElapsedP e ns) = fromCP ns $ rawGmTime e

-- | convert a unix epoch to DateTime
dateTimeFromUnixEpoch :: Elapsed -> DateTime
dateTimeFromUnixEpoch e = fromC $ rawGmTime e

-- | return the timezone offset in minutes
systemGetTimezone :: IO TimezoneOffset
systemGetTimezone = TimezoneOffset . fromIntegral . flip div 60 <$> localTime 0

----------------------------------------------------------------------------------------
-- | return the current elapsedP
systemGetElapsedP :: IO ElapsedP
systemGetElapsedP = allocaBytesAligned sofTimespec 8 $ \ptr -> do
    c_clock_get ptr
    toElapsedP <$> peek (castPtr ptr) <*> peekByteOff (castPtr ptr) sofCTime
  where sofTimespec = sofCTime + sofCLong
        sofCTime = sizeOf (0 :: CTime)
        sofCLong = sizeOf (0 :: CLong)
#if (MIN_VERSION_base(4,5,0))
        toElapsedP :: CTime -> CLong -> ElapsedP
        toElapsedP (CTime sec) nsec = ElapsedP (Elapsed $ Seconds (fromIntegral sec)) (fromIntegral nsec)
#else
        toElapsedP :: CLong -> CLong -> ElapsedP
        toElapsedP sec         nsec = ElapsedP (Elapsed $ Seconds (fromIntegral sec)) (fromIntegral nsec)
#endif

-- | return the current elapsed
systemGetElapsed :: IO Elapsed
systemGetElapsed = allocaBytesAligned sofTimespec 8 $ \ptr -> do
    c_clock_get ptr
    toElapsed <$> peek (castPtr ptr)
  where sofTimespec = sizeOf (0 :: CTime) + sizeOf (0 :: CLong)
#if (MIN_VERSION_base(4,5,0))
        toElapsed :: CTime -> Elapsed
        toElapsed (CTime sec) = Elapsed $ Seconds (fromIntegral sec)
#else
        toElapsed :: CLong -> Elapsed
        toElapsed sec         = Elapsed $ Seconds (fromIntegral sec)
#endif

foreign import ccall unsafe "hourglass_clock_calendar"
    c_clock_get :: Ptr CLong -> IO ()

#if (MIN_VERSION_base(4,5,0))
foreign import ccall unsafe "gmtime_r"
    c_gmtime_r :: Ptr CTime -> Ptr CTm -> IO (Ptr CTm)

foreign import ccall unsafe "localtime_r"
    c_localtime_r :: Ptr CTime -> Ptr CTm -> IO (Ptr CTm)
#else
foreign import ccall unsafe "gmtime_r"
    c_gmtime_r :: Ptr CLong -> Ptr CTm -> IO (Ptr CTm)

foreign import ccall unsafe "localtime_r"
    c_localtime_r :: Ptr CLong -> Ptr CTm -> IO (Ptr CTm)
#endif

-- | Return a global time's struct tm based on the number of elapsed second since unix epoch.
rawGmTime :: Elapsed -> CTm
rawGmTime (Elapsed (Seconds s)) = unsafePerformIO callTime
  where callTime =
            alloca $ \ctmPtr -> do
            alloca $ \ctimePtr -> do
                poke ctimePtr ctime
                r <- c_gmtime_r ctimePtr ctmPtr
                if r == nullPtr
                    then error "gmTime failed"
                    else peek ctmPtr
        ctime = fromIntegral s
{-# NOINLINE rawGmTime #-}

-- | Return a local time's gmtoff (seconds east of UTC)
--
-- use the ill defined gmtoff (at offset 40) that might or might not be
-- available for your platform. worst case scenario it's not initialized
-- properly.
localTime :: Elapsed -> IO CLong
localTime (Elapsed (Seconds s)) = callTime
  where callTime =
            alloca $ \ctmPtr -> do
            alloca $ \ctimePtr -> do
                poke ctimePtr ctime
                r <- c_localtime_r ctimePtr ctmPtr
                if r == nullPtr
                    then error "localTime failed"
                    else peekByteOff ctmPtr 40
        ctime = fromIntegral s

-- | Represent the beginning of struct tm
data CTm = CTm
    { ctmSec    :: CInt
    , ctmMin    :: CInt
    , ctmHour   :: CInt
    , ctmMDay   :: CInt
    , ctmMon    :: CInt
    , ctmYear   :: CInt
    } deriving (Show,Eq)

-- | Convert a C structure to a DateTime structure
fromC :: CTm -> DateTime
fromC ctm = DateTime date time
  where date = Date
            { dateYear  = fromIntegral $ ctmYear ctm + 1900
            , dateMonth = toEnum $ fromIntegral $ ctmMon ctm
            , dateDay   = fromIntegral $ ctmMDay ctm
            }
        time = TimeOfDay
            { todHour = fromIntegral $ ctmHour ctm
            , todMin  = fromIntegral $ ctmMin ctm
            , todSec  = fromIntegral $ ctmSec ctm
            , todNSec = 0
            }

-- | Similar to 'fromC' except with nanosecond precision
fromCP :: NanoSeconds -> CTm -> DateTime
fromCP ns ctm = DateTime d (t { todNSec = ns })
  where (DateTime d t) = fromC ctm

instance Storable CTm where
    alignment _ = 8
    sizeOf _    = 60 -- account for 9 ints, alignment + 2 unsigned long at end.
    peek ptr    = do
        CTm <$> peekByteOff intPtr 0
            <*> peekByteOff intPtr 4
            <*> peekByteOff intPtr 8
            <*> peekByteOff intPtr 12
            <*> peekByteOff intPtr 16
            <*> peekByteOff intPtr 20
      where intPtr = castPtr ptr
    poke ptr (CTm f0 f1 f2 f3 f4 f5) = do
        mapM_ (uncurry (pokeByteOff intPtr))
            [(0,f0), (4,f1), (8,f2), (12,f3), (16,f4), (20,f5)]
        --pokeByteOff (castPtr ptr) 36 f9
      where intPtr = castPtr ptr