-- | A simple implementation of flicks, using 64-bit integers.
-- See https://github.com/OculusVR/Flicks#README for the spec.
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Time.Flick
  ( -- * The Flicks data type
    Flicks(..)
  , flicksPerSecond, secnd
    -- * Conversions
  , approxFlicks
  , periodForFreq
  , timeSpecToFlicks
  , flicksToDiffTime
    -- * Measuring time in flicks
  , flicksNow
  , threadDelayFlicks
  ) where

import Control.Concurrent (threadDelay)
import Data.Fixed
import Data.Int
import Data.Proxy
import Data.Ratio

import Data.Time.Clock
import System.Clock

-- | Time measured in units of flicks. One flick is precisely
-- @1/705600000@ seconds. Many common frame rates produce
-- frame times which are an integer number of flicks.
newtype Flicks = Flicks { unFlicks :: Int64 }
  deriving newtype (Eq, Ord, Show, Num, Enum, Integral, Real)

-- | How many flicks are in a second. Precisely 705600000.
flicksPerSecond :: Num a => a
flicksPerSecond = 705600000

-- | One second in flicks.
secnd :: Flicks
secnd = flicksPerSecond

-- | Convert a number of seconds into flicks, rounding towards zero.
approxFlicks :: Rational -> Flicks
approxFlicks t = Flicks . truncate $ t * flicksPerSecond

-- | Duration in flicks of one oscillation at the given frequency (in Hertz).
-- The result is rounded towards zero.
periodForFreq :: Rational -> Flicks
periodForFreq = approxFlicks . recip

-- | Convert a @'TimeSpec'@ into flicks. Rounds towards zero.
timeSpecToFlicks :: TimeSpec -> Flicks
timeSpecToFlicks TimeSpec{sec, nsec} = Flicks (secPart + nsecPart)
  where
    secPart = flicksPerSecond * sec
    nsecPart = (flicksPerSecond * nsec) `quot` 10^9

-- | Convert flicks into seconds with some amount of precision.
flicksToFixed :: forall r. HasResolution r => Flicks -> Fixed r
flicksToFixed (Flicks t) = MkFixed val
  where
    val = fromIntegral t * resolution (Proxy @r) `quot` flicksPerSecond

-- | Convert flicks into @'DiffTime'@
flicksToDiffTime :: Flicks -> DiffTime
flicksToDiffTime t = picosecondsToDiffTime picos
  where
    MkFixed picos = flicksToFixed t :: Fixed E12

-- | Get the current time in flicks. The epoch is an arbitrary starting
-- point and the maginute of the result should not be relied on.
-- 
-- See the documentation of @"System.Clock"@ for details.
flicksNow :: Clock -> IO Flicks
flicksNow clockTy = timeSpecToFlicks <$> getTime clockTy

-- | Suspend the current thread for a given duration.
-- Inherits 'threadDelay' 's lack of guarantees.
threadDelayFlicks :: Flicks -> IO ()
threadDelayFlicks (Flicks t) = do
  let micros = fromIntegral $ (10^6 * t) `quot` flicksPerSecond
  threadDelay micros