{-# LANGUAGE GADTs #-}
module System.Delta.Callback ( CallbackWatcher
                             , CallbackId
                               
                             -- * Construction
                             , withCallbacks
                               
                             -- * Adding callbacks
                             , withDeleteCallback
                             , withChangedCallback
                             , withNewCallback

                             -- * Removing callbacks
                             , unregisterCallback
                             , removeAllCallbacks

                             -- * Closing the watcher
                             , closeCallbackWatcher

                             -- * Helper functions
                             , callbackOnEvent
                             )where

import FRP.Sodium
import System.Delta.Base
import System.Delta.Class

import qualified Data.Map as M

import Control.Concurrent.MVar

-- | Id of a callback in a 'CallbackWatcher'
newtype CallbackId = CallbackId Integer
                   deriving (Eq, Ord)  

-- | Provides a callback based interface to an FRP base 'FileWatcher'
data CallbackWatcher where
  CallbackWatcher :: {
    baseWatcher :: FileWatcher
  , nextCallbackId :: MVar CallbackId
  , watcherCallbacks :: MVar (M.Map CallbackId (IO ()))
  } -> CallbackWatcher


-- | Raise the callback id of a callback watcher
raiseId :: CallbackWatcher -> IO (CallbackId)
raiseId w = do
  (CallbackId n) <- takeMVar $ nextCallbackId w
  putMVar (nextCallbackId w) (CallbackId $ n+1)
  return (CallbackId n)

-- | Add an action to unregister a callback
addCallbackUnregister :: CallbackWatcher -> IO () -> IO CallbackId
addCallbackUnregister w removeCallback = do
  newId <- raiseId w
  mp <- takeMVar $ watcherCallbacks w
  putMVar (watcherCallbacks w) (M.insert newId removeCallback mp)
  return newId
  
-- | Wrap a file watcher in a datatype that allows adding callbacks
withCallbacks :: FileWatcher -> IO CallbackWatcher
withCallbacks w = do
  nextIdVar <- newMVar (CallbackId 0)
  callbacks <- newMVar (M.empty)
  return $ CallbackWatcher w nextIdVar callbacks

-- | Add a callback that is executed when file deletion is detected
withDeleteCallback :: CallbackWatcher
                   -> (FilePath -> IO ()) -- ^ An IO action on the deleted path
                   -> IO (CallbackId)
withDeleteCallback watcher action = do
  unregisterCallback <- callbackOnEvent (deletedFiles $ baseWatcher watcher) action
  addCallbackUnregister watcher unregisterCallback

-- | Add a callback that is executed when file creation is detected
withNewCallback :: CallbackWatcher
                -> (FilePath -> IO ()) -- ^ An IO action on the new path
                -> IO (CallbackId)
withNewCallback watcher action = do
  unregisterCallback <- callbackOnEvent (newFiles $ baseWatcher watcher) action
  addCallbackUnregister watcher unregisterCallback

-- | Add a callback on a changed file
withChangedCallback :: CallbackWatcher
                    -> (FilePath -> IO ()) -- ^ Action on changed file
                    -> IO (CallbackId)
withChangedCallback watcher action = do
  unregisterCallback <- callbackOnEvent (changedFiles $ baseWatcher watcher) action
  addCallbackUnregister watcher unregisterCallback

-- | Unregister the given CallbackId from the FileWatcher
-- does nothing if the CallbackId is not in the watcher
unregisterCallback :: CallbackWatcher -> CallbackId -> IO ()
unregisterCallback watcher cId = do
  mp <- takeMVar $ watcherCallbacks watcher
  case M.lookup cId mp of
    Nothing -> return ()
    Just action -> action
  putMVar (watcherCallbacks watcher) (M.delete cId mp)

-- | Remove all callbacks form the watcher. They will not be called after this
removeAllCallbacks :: CallbackWatcher -> IO ()
removeAllCallbacks watcher = do
  mp <- takeMVar $ watcherCallbacks watcher
  putMVar (watcherCallbacks watcher) M.empty
  sequence_ (M.elems mp)

-- | Remove all callbacks and close the underlying FileWatcher
closeCallbackWatcher :: CallbackWatcher -> IO ()
closeCallbackWatcher watcher = do
  removeAllCallbacks watcher
  cleanUpAndClose $ baseWatcher watcher
  

-- | Add a listener to an event, return the action to unregister the listener
callbackOnEvent :: Event a -> (a -> IO ()) -> IO (IO ())
callbackOnEvent e action = sync $ listen e action