{-| Contains functions for composing units of time and signals that sample from the game clock. -}
module FRP.Helm.Time (
  -- * Units
  Time,
  millisecond,
  second,
  minute,
  hour,
  inMilliseconds,
  inSeconds,
  inMinutes,
  inHours,
  -- * Tickers
  fps,
  fpsWhen,
  every,
  -- * Timing
  timestamp,
  delay,
  since
) where

import Control.Applicative
import Control.Monad
import FRP.Elerea.Param hiding (delay, Signal, until)
import qualified FRP.Elerea.Param as Elerea (Signal, until)
import Data.Time.Clock.POSIX (getPOSIXTime)
import FRP.Helm.Signal
import FRP.Helm.Sample
import System.IO.Unsafe (unsafePerformIO)

{-| A type describing an amount of time in an arbitary unit. Use the time
    composing/converting functions to manipulate time values. -}
type Time = Double

{-| A time value representing one millisecond. -}
millisecond :: Time
millisecond = 1

{-| A time value representing one second. -}
second :: Time
second = 1000

{-| A time value representing one minute. -}
minute :: Time
minute = 60000

{-| A time value representing one hour. -}
hour :: Time
hour = 3600000

{-| Converts a time value to a fractional value, in milliseconds. -}
inMilliseconds :: Time -> Double
inMilliseconds n = n

{-| Converts a time value to a fractional value, in seconds. -}
inSeconds :: Time -> Double
inSeconds n = n / second

{-| Converts a time value to a fractional value, in minutes. -}
inMinutes :: Time -> Double
inMinutes n = n / minute

{-| Converts a time value to a fractional value, in hours. -}
inHours :: Time -> Double
inHours n = n / hour

{-| Takes desired number of frames per second (fps). The resulting signal gives
   a sequence of time deltas as quickly as possible until it reaches the
   desired FPS. A time delta is the time between the last frame and the current
   frame. -}
fps :: Double -> Signal Time
fps n = snd <~ every' t
  where --Ain't nobody got time for infinity
    t = if n == 0 then 0 else second / n

{-| Same as the fps function, but you can turn it on and off. Allows you to do
   brief animations based on user input without major inefficiencies. The first
   time delta after a pause is always zero, no matter how long the pause was.
   This way summing the deltas will actually give the amount of time that the
   output signal has been running. -}
fpsWhen :: Double -> Signal Bool -> Signal Time
fpsWhen n sig = Signal $ do c <- signalGen sig
                            f <- signalGen (fps n)
                            transfer2 (pure 0) update_ f c
  where update_ _ new (Unchanged cont) old = if cont
                                             then new
                                             else Unchanged $ value old
        update_ _ _   (Changed   cont) old = if cont
                                             then Changed 0
                                             else Unchanged $ value old
{-| Takes a time interval t. The resulting signal is the current time, updated
    every t. -}
every :: Time -> Signal Time
every t = fst <~ every' t

{-| A utility signal used by 'fps' and 'every' that returns the current time
    and a delta every t. -}
every' :: Time -> Signal (Time, Time)
every' t = Signal $ every'' t >>= transfer (pure (0,0)) update

{-| Another utility signal that does all the magic for 'every'' by working on
    the Elerea SignalGen level -}
every'' :: Time -> SignalGen p (Elerea.Signal (Time, Time))
every'' t = getTime >>= transfer (0,0) update_
  where
    getTime = effectful $ liftM ((second *) . realToFrac) getPOSIXTime
    update_ _ new old = let delta = new - fst old
                        in if delta >= t then (new, delta) else old

{-| Add a timestamp to any signal. Timestamps increase monotonically. When you
    create (timestamp Mouse.x), an initial timestamp is produced. The timestamp
    updates whenever Mouse.x updates.

    Unlike in Elm the timestamps are not tied to the underlying signals so the
    timestamps for Mouse.x and  Mouse.y will be slightly different. -}
timestamp :: Signal a -> Signal (Time, a)
timestamp = lift2 (,) pure_time
  where pure_time = fst <~ (Signal $ (fmap . fmap) pure (every'' millisecond))

{-| Delay a signal by a certain amount of time. So (delay second Mouse.clicks)
    will update one second later than any mouse click. -}
delay :: Time -> Signal a -> Signal a
delay t (Signal gen) = Signal $ (fmap . fmap) fst $
                         do s <- gen
                            w <- timeout
                            e <- snapshot =<< input
                            transfer2 (makeInit e, []) update_ w s
  where
     -- XXX uses unsafePerformIO, is there a better way?
    makeInit e = pure $ value $ unsafePerformIO (start gen >>= (\f -> f e))
    update_ _ waiting new (old, olds) = if waiting then (old, new:olds)
                                        else (last olds, new:init olds)
    timeout = every'' t >>= transfer False (\_ (time,delta) _ -> time /= delta)
                        -- 'Elerea.until' will lose the reference to the input so
                        -- we don't keep looking up the time even though the
                        -- output can never change again
                        >>= Elerea.until
                        >>= transfer True (\_ new old -> old && not new)

{-| Takes a time t and any signal. The resulting boolean signal is true for
    time t after every event on the input signal. So (second `since`
    Mouse.clicks) would result in a signal that is true for one second after
    each mouse click and false otherwise. -}
since :: Time -> Signal a -> Signal Bool
since t s = lift2 (/=) (count s) (count (delay t s))