{-# LANGUAGE ScopedTypeVariables #-}

-- | Unstable API which exposes internals for testing.
module Control.Debounce.Internal (
  DebounceSettings(..)
  , DebounceEdge(..)
  , leadingEdge
  , trailingEdge
  , mkDebounceInternal
  ) where

import           Control.Concurrent      (forkIO)
import           Control.Concurrent.MVar (takeMVar, tryPutMVar, tryTakeMVar, MVar)
import           Control.Exception       (SomeException, handle, mask_)
import           Control.Monad           (forever, void)

-- | Settings to control how debouncing should work.
--
-- This should be constructed using 'defaultDebounceSettings' and record
-- update syntax, e.g.:
--
-- @
-- let settings = 'defaultDebounceSettings' { 'debounceAction' = flushLog }
-- @
--
-- @since 0.1.2
data DebounceSettings = DebounceSettings
    { DebounceSettings -> Int
debounceFreq   :: Int
    -- ^ Length of the debounce timeout period in microseconds.
    --
    -- Default: 1 second (1000000)
    --
    -- @since 0.1.2
    , DebounceSettings -> IO ()
debounceAction :: IO ()
    -- ^ Action to be performed.
    --
    -- Note: all exceptions thrown by this action will be silently discarded.
    --
    -- Default: does nothing.
    --
    -- @since 0.1.2
    , DebounceSettings -> DebounceEdge
debounceEdge :: DebounceEdge
    -- ^ Whether to perform the action on the leading edge or trailing edge of
    -- the timeout.
    --
    -- Default: 'trailingEdge'.
    --
    -- @since 0.1.6
    }

-- | Setting to control whether the action happens at the leading and/or trailing
-- edge of the timeout.
--
-- @since 0.1.6
data DebounceEdge =
  Leading
  -- ^ Perform the action immediately, and then begin a cooldown period.
  -- If the trigger happens again during the cooldown, wait until the end of the cooldown
  -- and then perform the action again, then enter a new cooldown period.
  | Trailing
  -- ^ Start a cooldown period and perform the action when the period ends. If another trigger
  -- happens during the cooldown, it has no effect.
  deriving (Int -> DebounceEdge -> ShowS
[DebounceEdge] -> ShowS
DebounceEdge -> String
(Int -> DebounceEdge -> ShowS)
-> (DebounceEdge -> String)
-> ([DebounceEdge] -> ShowS)
-> Show DebounceEdge
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> DebounceEdge -> ShowS
showsPrec :: Int -> DebounceEdge -> ShowS
$cshow :: DebounceEdge -> String
show :: DebounceEdge -> String
$cshowList :: [DebounceEdge] -> ShowS
showList :: [DebounceEdge] -> ShowS
Show, DebounceEdge -> DebounceEdge -> Bool
(DebounceEdge -> DebounceEdge -> Bool)
-> (DebounceEdge -> DebounceEdge -> Bool) -> Eq DebounceEdge
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: DebounceEdge -> DebounceEdge -> Bool
== :: DebounceEdge -> DebounceEdge -> Bool
$c/= :: DebounceEdge -> DebounceEdge -> Bool
/= :: DebounceEdge -> DebounceEdge -> Bool
Eq)


-- | Perform the action immediately, and then begin a cooldown period.
-- If the trigger happens again during the cooldown, wait until the end of the cooldown
-- and then perform the action again, then enter a new cooldown period.
--
-- @since 0.1.6
leadingEdge :: DebounceEdge
leadingEdge :: DebounceEdge
leadingEdge = DebounceEdge
Leading

-- | Start a cooldown period and perform the action when the period ends. If another trigger
-- happens during the cooldown, it has no effect.
--
-- @since 0.1.6
trailingEdge :: DebounceEdge
trailingEdge :: DebounceEdge
trailingEdge = DebounceEdge
Trailing

mkDebounceInternal :: MVar () -> (Int -> IO ()) -> DebounceSettings -> IO (IO ())
mkDebounceInternal :: MVar () -> (Int -> IO ()) -> DebounceSettings -> IO (IO ())
mkDebounceInternal MVar ()
baton Int -> IO ()
delayFn (DebounceSettings Int
freq IO ()
action DebounceEdge
edge) = do
    IO () -> IO ()
forall a. IO a -> IO a
mask_ (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ IO ThreadId -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO ThreadId -> IO ()) -> IO ThreadId -> IO ()
forall a b. (a -> b) -> a -> b
$ IO () -> IO ThreadId
forkIO (IO () -> IO ThreadId) -> IO () -> IO ThreadId
forall a b. (a -> b) -> a -> b
$ IO () -> IO ()
forall (f :: * -> *) a b. Applicative f => f a -> f b
forever (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
        MVar () -> IO ()
forall a. MVar a -> IO a
takeMVar MVar ()
baton
        case DebounceEdge
edge of
          DebounceEdge
Leading -> do
            IO () -> IO ()
ignoreExc IO ()
action
            Int -> IO ()
delayFn Int
freq
          DebounceEdge
Trailing -> do
            Int -> IO ()
delayFn Int
freq
            -- Empty the baton of any other activations during the interval
            IO (Maybe ()) -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO (Maybe ()) -> IO ()) -> IO (Maybe ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ MVar () -> IO (Maybe ())
forall a. MVar a -> IO (Maybe a)
tryTakeMVar MVar ()
baton
            IO () -> IO ()
ignoreExc IO ()
action

    IO () -> IO (IO ())
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return (IO () -> IO (IO ())) -> IO () -> IO (IO ())
forall a b. (a -> b) -> a -> b
$ IO Bool -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (IO Bool -> IO ()) -> IO Bool -> IO ()
forall a b. (a -> b) -> a -> b
$ MVar () -> () -> IO Bool
forall a. MVar a -> a -> IO Bool
tryPutMVar MVar ()
baton ()

ignoreExc :: IO () -> IO ()
ignoreExc :: IO () -> IO ()
ignoreExc = (SomeException -> IO ()) -> IO () -> IO ()
forall e a. Exception e => (e -> IO a) -> IO a -> IO a
handle ((SomeException -> IO ()) -> IO () -> IO ())
-> (SomeException -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ \(SomeException
_ :: SomeException) -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()