-- | 
-- Module: Common
-- Description: Common functions.
-- Copyright: (c) 2013 Tom Hawkins & Lee Pike
--
-- Common Atom functions

module Language.Atom.Common
  (
  -- * Timers
    Timer
  , timer
  , startTimer
  , startTimerIf
  , timerDone
  -- * One Shots
  , oneShotRise
  , oneShotFall
  -- * Debouncing
  , debounce
  -- * Lookup Tables
  , lookupTable
  , linear
  -- * Hysteresis
  , hysteresis
  -- * Channels
  , Channel (..)
  , channel
  , writeChannel
  , readChannel
  ) where

import Data.Word

import Language.Atom.Language

-- | A Timer.
data Timer = Timer (V Word64)

-- | Creates a new timer.
timer :: Name -> Atom Timer
timer name = do
  timer' <- word64 name 0
  return $ Timer timer'

-- | Starts a Timer.  A timer can be restarted at any time.
startTimer :: Timer -- ^ Timer to start
           -> E Word64 -- ^ Number of clock ticks the timer shall run
           -> Atom ()
startTimer t = startTimerIf t true

-- | Conditionally start a Timer.
startTimerIf :: Timer -- ^ Timer to start conditionally
             -> E Bool -- ^ Condition for starting the timer
             -> E Word64 -- ^ Number of ticks the timer shall run
             -> Atom ()
startTimerIf (Timer t) a time = t <== mux a (clock + time) (value t)

-- | 'True' when a timer has completed. Note that this remains 'True' until
-- the timer is restarted.
timerDone :: Timer -> E Bool
timerDone (Timer t) = value t <=. clock

-- | One-shot on a rising transition.
oneShotRise :: E Bool -> Atom (E Bool)
oneShotRise a = do
  last' <- bool "last" False
  last' <== a
  return $ a &&. not_ (value last')

-- | One-shot on a falling transition.
oneShotFall :: E Bool -> Atom (E Bool)
oneShotFall = oneShotRise . not_

-- | Debounces a boolean given an on and off time (ticks) and an initial state.
debounce :: Name -- ^ Name of the resulting atom
         -> E Word64 -- ^ On time in ticks
         -> E Word64 -- ^ Off time in ticks
         -> Bool -- ^ Initial value
         -> E Bool -- ^ The boolean to debounce
         -> Atom (E Bool) -- ^ Resulting debounced boolean
debounce name onTime offTime init' a = atom name $ do
  lst  <- bool "last" init'
  out   <- bool "out"  init'
  timer' <- timer "timer"
  atom "on" $ do
    cond $ a &&. not_ (value lst)
    startTimer timer' onTime
    lst <== a
  atom "off" $ do
    cond $ not_ a &&. value lst
    startTimer timer' offTime
    lst <== a
  atom "set" $ do
    cond $ a ==. value lst
    cond $ timerDone timer'
    out <== value lst
  return $ value out

-- | 1-D lookup table.  @x@ values out of table range are clipped at end @y@
-- values.  Input table must be monotonically increasing in @x@.
lookupTable :: FloatingE a => [(E a, E a)] -- ^ (@x@, @y@) lookup table
               -> E a -- ^ Input @x@ value
               -> E a -- ^ Output @y@ value
lookupTable table x = mux (x >=. x1) y1 $ foldl f y0 table'
  where
  (_,  y0) = head table
  (x1, y1) = last table
  table' = zip (init table) (tail table)
  f a ((a0,b0),(a1,b1)) = mux (x >=. a0) interp a
    where
    slope = (b1 - b0) / (a1 - a0)
    interp = (x - a0) * slope + b0

-- | Linear extrapolation and interpolation on a line with 2 points.
-- The two @x@ points must be different to prevent a divide-by-zero.
linear :: FloatingE a => (E a, E a) -- ^ First point, (x1, y1)
          -> (E a, E a) -- ^ Second point, (x2, y2)
          -> E a -- ^ Input @x@ value
          -> E a -- ^ Interpolated/extrapolated @y@ value
linear (x1, y1) (x2, y2) a = slope * a + inter
  where
  slope = (y2 - y1) / (x2 - x1)
  inter = y1 - slope * x1

-- | Hysteresis returns 'True' when the input exceeds @max@ and 'False' when
-- the input is less than @min@.  The state is held when the input is between
-- @min@ and @max@.
hysteresis :: OrdE a => E a -- ^ min
              -> E a -- ^ max
              -> E a -- ^ Input
              -> Atom (E Bool)
hysteresis a b u = do
  s <- bool "s" False
  s <== (mux (u >. max') true $ mux (u <. min') false $ value s)
  return $ value s
  where
  min' = min_ a b
  max' = max_ a b

-- | A channel is a uni-directional communication link that ensures one read
-- for every write.
data Channel a = Channel a (V Bool)

-- | Creates a new channel, with a given name and data.
channel :: a -> Atom (Channel a)
channel a = do
  hasData <- bool "hasData" False
  return $ Channel a hasData

-- | Write data to a 'Channel'.  A write will only suceed if the 'Channel' is
-- empty.
writeChannel :: Channel a -> Atom ()
writeChannel (Channel _ hasData) = do
  cond $ not_ $ value hasData
  hasData <== true

-- | Read data from a 'Channel'.  A read will only suceed if the 'Channel' has
-- data to be read.
readChannel :: Channel a -> Atom a
readChannel (Channel a hasData) = do
  cond $ value hasData
  hasData <== false
  return a

{-
module Language.Atom.Common.Process
  ( Process (..)
  , process
  ) where

import Language.Atom

data Process
  = Par [Process]
  | Seq [Process]
  | Alt [Process]
  | Rep Process
  | Act Action

process :: Name -> Process -> Atom ()

-}