{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE ExplicitForAll             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- | This module introduces 'Timestamp' data type
-- and corresponding functions for operations with time.

module Time.Timestamp
       ( Timestamp (..)
       , fromUnixTime
       , timeDiff
       , timeAdd
       , timeMul
       , (*:*)
       , timeDiv
       , (/:/)

         -- * Other operators
       , (+:+)
       , (-:-)
       , (-%-)

       ) where

import Time.Rational (KnownDivRat, KnownRat, Rat, RatioNat)
import Time.Units (Second, Time (..), sec, toUnit)

-- $setup
-- >>> import Time.Units (Minute, Second, minute, ms, sec)

-- | Similar to 'Time' but has no units and can be negative.
newtype Timestamp = Timestamp Rational
    deriving (Show, Read, Eq, Ord)

-- | Converts unix time to 'Timestamp'.
fromUnixTime :: Real a => a -> Timestamp
fromUnixTime = Timestamp . toRational

{- | Returns the result of comparison of two 'Timestamp's and
the 'Time' of that difference of given time unit.

>>> timeDiff @Second (Timestamp 4) (Timestamp 2)
(GT,2s)

>>>timeDiff @Minute (Timestamp 4) (Timestamp 2)
(GT,1/30m)

>>> timeDiff @Second (Timestamp 2) (Timestamp 4)
(LT,2s)

>>> timeDiff @Minute (Timestamp 2) (Timestamp 4)
(LT,1/30m)

-}
timeDiff :: forall (unit :: Rat) . KnownDivRat Second unit
         => Timestamp
         -> Timestamp
         -> (Ordering, Time unit)
timeDiff (Timestamp a) (Timestamp b) =
    let (order, r) = ratDiff a b
    in (order, toUnit $ sec $ fromRational r)

{- | Returns the result of addition of 'Time' with 'Timestamp' elements.

>>> sec 5 `timeAdd` (Timestamp 4)
Timestamp (9 % 1)

>>> minute 1 `timeAdd` (Timestamp 5)
Timestamp (65 % 1)

-}
timeAdd :: forall (unit :: Rat) . KnownDivRat unit Second
        => Time unit
        -> Timestamp
        -> Timestamp
timeAdd t (Timestamp ts) = Timestamp (toRational (unTime $ toUnit @Second t) + ts)

-- | Returns the result of multiplication of two 'Time' elements.
timeMul :: forall (unit :: Rat) . KnownRat unit
        => RatioNat
        -> Time unit
        -> Time unit
timeMul n (Time t) = Time (n * t)

{- | Operator version of 'timeMul'.

>>> 3 *:* sec 5
15s

>>> 2 *:* 3 *:* sec 5
30s

>>> 3 *:* 5 *:* 7 :: Time Second
105s

>>> ms 2000 +:+ 2 *:* sec 3
8s

-}
infixr 7 *:*
(*:*) :: forall (unit :: Rat) . KnownRat unit
      => RatioNat -> Time unit -> Time unit
(*:*) = timeMul

-- | Returns the result of division of two 'Time' elements.
timeDiv :: forall (unit :: Rat) . KnownRat unit
        => Time unit
        -> Time unit
        -> RatioNat
timeDiv (Time t1) (Time t2) = t1 / t2

{- | Operator version of 'timeDiv'.

>>> sec 15 /:/ sec 3
5 % 1

-}
infix 7 /:/
(/:/) :: forall (unit :: Rat) . KnownRat unit
      => Time unit -> Time unit -> RatioNat
(/:/) = timeDiv

-- | Sums times of different units.
--
-- >>> minute 1 +:+ sec 1
-- 61s
--
infixl 6 +:+
(+:+) :: forall (unitResult :: Rat) (unitLeft :: Rat) . KnownDivRat unitLeft unitResult
      => Time unitLeft
      -> Time unitResult
      -> Time unitResult
t1 +:+ t2 = toUnit t1 + t2
{-# INLINE (+:+) #-}

-- | Substracts times of different units.
--
-- >>> minute 1 -:- sec 1
-- 59s
--
infixl 6 -:-
(-:-) :: forall (unitResult :: Rat) (unitLeft :: Rat) . KnownDivRat unitLeft unitResult
      => Time unitLeft
      -> Time unitResult
      -> Time unitResult
t1 -:- t2 = toUnit t1 - t2
{-# INLINE (-:-) #-}

{- | Similar to '-:-' but more safe and returns order in pair with resulting time difference.

>>> sec 5 -%- sec 3
(GT,2s)

>>> sec 5 -%- sec 6
(LT,1s)

-}
infix 6 -%-
(-%-) :: forall (unitResult :: Rat) (unitLeft :: Rat) . KnownDivRat unitLeft unitResult
      => Time unitLeft
      -> Time unitResult
      -> (Ordering, Time unitResult)
t1 -%- (Time t2Rat) =
    let (Time t1Rat) = toUnit @unitResult t1
        (order, rat) = ratDiff (toRational t1Rat) (toRational t2Rat)
    in (order, Time $ fromRational rat)

ratDiff :: Rational -> Rational -> (Ordering, Rational)
ratDiff r1 r2 =
    let order = compare r1 r2
        diff  = case order of
                     LT -> r2 - r1
                     GT -> r1 - r2
                     EQ -> 0
    in (order, diff)