-- | 'UTime' is a minimalistic efficient alternative to 'UTCTime'.
--
-- The 'UTime' representation is intentionally not opaque to allow
-- efficient low-level access. But, you should consider this module
-- \"somewhat internal\".

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Data.UTime (
  UTime(..),
  fromUTime,
  toUTime,
  toUTimeUp, toUTimeUp',
  toUTimeDown, toUTimeDown',
  utime,
  -- * For testing only:
  dtPico,
  ) where

import Control.Lens
import Data.Time
import Data.Int
import Foreign.Storable
import Unsafe.Coerce

newtype UTime = UTime Int64
                deriving (Eq, Ord, Enum, Bounded, Storable)

-- 'DiffTime' is currently implemented as a newtype around
-- 'Integer'. As long as it stays so, this function is safe; and if it
-- changes it will blow up noticeably.
--
-- Because the appropriate accessors/constructors are not exported,
-- all other conversion mechanisms involve unnecessary 'Rational'
-- arithmetics, which is probably the biggest source of inefficiency
-- in 'UTCTime'.
dtPico :: Iso' DiffTime Integer
dtPico = iso unsafeCoerce unsafeCoerce
{-# INLINE dtPico #-}

fromUTime :: UTime -> UTCTime
fromUTime (UTime micros) = UTCTime (ModifiedJulianDay day) dayTime
  where
    (d, dt) = micros `divMod` 86400000000
    day = fromIntegral $ 40587 + d
    dayTime = (fromIntegral $ 1000000 * dt) ^. from dtPico
{-# INLINE fromUTime #-}

toUTimeGeneric :: Int64 -> UTCTime -> UTime
toUTimeGeneric shift (UTCTime (ModifiedJulianDay day) dayTime) = UTime micros
  where
    micros = d * 86400000000 + dt
    d = fromIntegral day - 40587
    dt = (fromIntegral (dayTime ^. dtPico) + shift) `div` 1000000
{-# INLINE toUTimeGeneric #-}

-- | Converts 'UTCTime' to 'UTime' by rounding.
toUTime :: UTCTime -> UTime
toUTime = toUTimeGeneric 500000
-- TODO(klao): rounding is probably the right thing to do
-- here. But, truncating can make things cleaner and easier to
-- reason about...
{-# INLINE toUTime #-}

-- | Returns the smallest 'UTime' value representing a time not
-- earlier than the input 'UTCTime'.
toUTimeUp :: UTCTime -> UTime
toUTimeUp = toUTimeGeneric 999999
{-# INLINE toUTimeUp #-}

-- | Returns the smallest 'UTime' value representing a time strictly
-- later than the input 'UTCTime'.
toUTimeUp' :: UTCTime -> UTime
toUTimeUp' = toUTimeGeneric 1000000
{-# INLINE toUTimeUp' #-}

-- | Returns the largest 'UTime' value representing a time not
-- later than the input 'UTCTime'.
toUTimeDown :: UTCTime -> UTime
toUTimeDown = toUTimeGeneric 0
{-# INLINE toUTimeDown #-}

-- | Returns the largest 'UTime' value representing a time strictly
-- earlier than the input 'UTCTime'.
toUTimeDown' :: UTCTime -> UTime
toUTimeDown' = toUTimeGeneric (-1)
{-# INLINE toUTimeDown' #-}

utime :: Iso' UTCTime UTime
utime = iso toUTime fromUTime
{-# INLINE utime #-}

instance Show UTime where
  showsPrec p = showsPrec p . fromUTime
  show = show . fromUTime