{-# LANGUAGE DeriveDataTypeable   #-}
{-# LANGUAGE TemplateHaskell      #-}
{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE BangPatterns         #-}
{-# LANGUAGE DeriveGeneric        #-}

module SafeCounter
  ( startCounter,
    getCount,
    getCountAsync,
    incCount,
    resetCount,
    wait,
    waitTimeout,
    Fetch(..),
    Increment(..),
    Reset(..)
  ) where

import Control.Distributed.Process hiding (call, say)
import Control.Distributed.Process.Extras
import Control.Distributed.Process.Async
import Control.Distributed.Process.ManagedProcess
  ( ProcessDefinition(..)
  , InitHandler
  , InitResult(..)
  , defaultProcess
  , condition
  )
import qualified Control.Distributed.Process.ManagedProcess as ManagedProcess (serve)
import Control.Distributed.Process.ManagedProcess.Client
import Control.Distributed.Process.ManagedProcess.Server.Restricted
import Control.Distributed.Process.Extras.Time
import Control.Distributed.Process.Serializable
import Data.Binary
import Data.Typeable (Typeable)
import GHC.Generics

--------------------------------------------------------------------------------
-- Types                                                                      --
--------------------------------------------------------------------------------

data Increment = Increment
  deriving (Show, Typeable, Generic)
instance Binary Increment where

data Fetch = Fetch
  deriving (Show, Typeable, Generic)
instance Binary Fetch where

data Reset = Reset deriving (Show, Typeable, Generic)
instance Binary Reset where

--------------------------------------------------------------------------------
-- API                                                                        --
--------------------------------------------------------------------------------

-- | Increment count
incCount :: ProcessId -> Process Int
incCount sid = call sid Increment

-- | Get the current count
getCount :: ProcessId -> Process Int
getCount sid = call sid Fetch

-- | Get the current count asynchronously
getCountAsync :: ProcessId -> Process (Async Int)
getCountAsync sid = callAsync sid Fetch

-- | Reset the current count
resetCount :: ProcessId -> Process ()
resetCount sid = cast sid Reset

-- | Start a counter server
startCounter :: Int -> Process ProcessId
startCounter startCount =
  let server = serverDefinition
  in spawnLocal $ ManagedProcess.serve startCount init' server
  where init' :: InitHandler Int Int
        init' count = return $ InitOk count Infinity

--------------------------------------------------------------------------------
-- Implementation                                                             --
--------------------------------------------------------------------------------

serverDefinition :: ProcessDefinition Int
serverDefinition = defaultProcess {
   apiHandlers = [
        handleCallIf
          (condition (\count Increment -> count >= 10)) -- invariant
          (\Increment -> halt :: RestrictedProcess Int (Result Int))

      , handleCall handleIncrement
      , handleCall (\Fetch -> getState >>= reply)
      , handleCast (\Reset -> putState (0 :: Int) >> continue)
      ]
   } :: ProcessDefinition Int

halt :: forall s r . Serializable r => RestrictedProcess s (Result r)
halt = haltNoReply (ExitOther "Count > 10")

handleIncrement :: Increment -> RestrictedProcess Int (Result Int)
handleIncrement _ = modifyState (+1) >> getState >>= reply

