{-# LANGUAGE Arrows #-}

module RSAGL.FRP.Accumulation
    (delay,
     integral,
     derivative,
     accumulateNumerical,
     integralRK4,
     integralRK4',
     summation,
     threadTime,
     sticky,
     initial,
     EdgeDetectionMode(..),
     edge,
     changed,
     clingy)
    where

import RSAGL.FRP.FRP
import RSAGL.FRP.Time
import RSAGL.FRP.RK4
import Control.Arrow
import RSAGL.Math.AbstractVector
import Data.Maybe

-- | Delay a piece of data for one frame.
delay :: x -> FRP e m x x
delay initial_value = accumulate (initial_value,error "delay: impossible") (\new_value (old_value,_) -> (new_value,old_value)) >>> arr snd

-- | Take the integral of a rate over time, using the trapezoidal rule.
integral :: (AbstractVector v,AbstractAdd p v) => p -> FRP e m (Rate v) p
integral initial_value = proc v ->
    do delta_t <- deltaTime -< ()
       (new_accum,_) <- accumulate (zero,perSecond zero) (\(delta_t,new_rate) (old_accum,old_rate) ->
           (old_accum `add` ((scalarMultiply (recip 2) $ new_rate `add` old_rate) `over` delta_t),new_rate)) -< (delta_t,v)
       returnA -< initial_value `add` new_accum

-- | Take the derivative of a value over time, by simple subtraction between frames.
derivative :: (AbstractVector v,AbstractSubtract p v) => FRP e m p (Rate v)
derivative = proc new_value ->
    do delta_t <- deltaTime -< ()
       m_old_value <- delay Nothing -< Just new_value
       let z = perSecond zero
       returnA -< maybe z (\old_value -> if delta_t == zero then z else (new_value `sub` old_value) `per` delta_t) m_old_value

-- | 'accumulate' harness for some numerical methods.
-- Parameters are: current input, previous output, delta time, absolute time, and number of frames at the specified frequency.
accumulateNumerical :: Frequency -> (i -> o -> Time -> Time -> Integer -> o) -> o -> FRP e m i o
accumulateNumerical frequency accumF initial_value = proc i ->
    do absolute_time <- absoluteTime -< ()
       delta_t <- deltaTime -< ()
       accumulate initial_value (\(i,absolute_time',delta_t',frames) o -> accumF i o absolute_time' delta_t' frames) -< 
           (i,absolute_time,delta_t,ceiling $ toSeconds delta_t / toSeconds (interval frequency))

integralRK4 :: (AbstractVector v) => Frequency -> (p -> v -> p) -> p -> FRP e m (Time -> p -> Rate v) p
integralRK4 f addPV = accumulateNumerical f (\diffF p abs_t delta_t -> integrateRK4 addPV diffF p (abs_t `sub` delta_t) abs_t)

integralRK4' :: (AbstractVector v) => Frequency -> (p -> v -> p) -> (p,Rate v) ->
                FRP e m (Time -> p -> Rate v -> Acceleration v) (p,Rate v)
integralRK4' f addPV = accumulateNumerical f (\diffF p abs_t delta_t -> integrateRK4' addPV diffF p (abs_t `sub` delta_t) abs_t)

-- | Sum some data frame-by-frame.
summation :: (AbstractAdd p v) => p -> FRP e m v p
summation initial_value = accumulate initial_value (\v p -> p `add` v)

-- | Elapsed time since the instantiation of this switch or thread.  Reset when a thread switches.
threadTime :: FRP e m () Time
threadTime = summation zero <<< deltaTime

-- | The edge detection mode.  If 'Discrete', detect edge between subsequent frames only.
-- If 'Fuzzy' detect edge since the most recent previous detected edge.
-- If 'HashedDiscrete', the comparison function is itself expensive, and the FRP runtime will compare by 'StableName's as a short-circuit optimization.
data EdgeDetectionMode = Fuzzy | Discrete

-- | Answer the most recent input that satisfies the predicate.
-- Accepts an initial value, which need not itself satisfy the predicate.
--
-- This can be a performance optimization, if it prevents unecessary evaluation of an input.
sticky :: (x -> Bool) -> x -> FRP e m x x
sticky f x = accumulate x (\new_x old_x -> if f new_x then new_x else old_x)

-- | Answer the first input that ever passes through a function.
initial :: FRP e m x x
initial = accumulate Nothing (\new_x m_old_x -> Just $ fromMaybe new_x m_old_x) >>> arr (fromMaybe $ error "initial: impossible happened")

-- | Returns 'True' only during frames on which the input has changed, based on a user-specified equality predicate.
-- The predicate function takes the most recent input as its first parameter.
edge :: EdgeDetectionMode -> (x -> x -> Bool) -> FRP e m x Bool
edge Discrete predicateF = proc x ->
    do d_x <- delay Nothing -< Just x
       returnA -< maybe True (not . predicateF x) d_x
edge Fuzzy predicateF = arr snd <<< accumulate (Nothing,error "changed: impossible")
                                    (\x_now (x_old,_) -> if maybe True (predicateF x_now) x_old
                                                         then (x_old,False)
                                                         else (Just x_now,True))

-- | Same as 'edge Discrete'.
changed :: (x -> x -> Bool) -> FRP e m x Bool
changed = edge Discrete

-- | Recalculate a function only at the edges of it's input.
clingy :: EdgeDetectionMode -> (j -> j -> Bool) -> (j -> p) -> FRP e m j p
clingy edm predicateF f = proc j ->
    do e <- edge edm predicateF -< j
       arr snd <<< sticky fst (error "clingy: impossible") -< (e,f j)