module PartialHandler where

import Prelude
import Control.Monad
import Control.Applicative
import Control.Exception
import Control.Concurrent
import Data.Maybe
import Data.Monoid
import System.IO.Error


-- |
-- A composable exception handler.
newtype PartialHandler a =
  PartialHandler (SomeException -> Maybe (IO a))


instance Functor PartialHandler where
  fmap fn (PartialHandler partialHandlerFn) =
    PartialHandler (fmap (fmap fn) . partialHandlerFn)

instance Applicative PartialHandler where
  pure x =
    PartialHandler (const (pure (pure x)))
  (<*>) (PartialHandler partialHandlerFn1) (PartialHandler partialHandlerFn2) =
    PartialHandler (liftA2 (liftA2 (liftA2 ($))) partialHandlerFn1 partialHandlerFn2)

instance Alternative PartialHandler where
  empty =
    PartialHandler (const Nothing)
  (<|>) (PartialHandler partialHandlerFn1) (PartialHandler partialHandlerFn2) =
    PartialHandler (liftA2 (<|>) partialHandlerFn1 partialHandlerFn2)

instance Monoid (PartialHandler a) where
  mempty = 
    empty
  mappend =
    (<|>)



-- * Totalizers
-------------------------

-- |
-- A function, which handles all exceptions.
-- 
-- Can be used as a parameter to standard functions like 'catch' and 'handle',
-- or to process the result of 'try'.
type TotalHandler a = 
  SomeException -> IO a

-- |
-- Convert a partial handler into a total handler,
-- which throws an error for unhandled cases.
-- In other words, the produced total handler is itself a partial function,
-- so use this only in cases when an unhandled exception should be considered as a bug.
{-# INLINABLE totalize #-}
totalize :: PartialHandler a -> TotalHandler a
totalize (PartialHandler h) =
  \e -> fromMaybe (error $ "Unhandled exception: " <> show e) (h e)

-- |
-- Convert a partial handler into a total handler,
-- which rethrows all unhandled exceptions.
{-# INLINABLE totalizeRethrowing #-}
totalizeRethrowing :: PartialHandler a -> TotalHandler a
totalizeRethrowing (PartialHandler h) =
  \e -> fromMaybe (throwIO e) (h e)

-- |
-- Convert a partial handler into a total handler,
-- which rethrows all unhandled exceptions to the specified thread and 
-- the current thread.
{-# INLINABLE totalizeRethrowingTo #-}
totalizeRethrowingTo :: ThreadId -> PartialHandler a -> TotalHandler a
totalizeRethrowingTo t (PartialHandler h) =
  \e -> fromMaybe (throwTo t e >> throwIO e) (h e)

-- |
-- Convert a partial handler into a total handler,
-- which rethrows all unhandled exceptions to the specified thread,
-- while the current thread continues the execution.
{-# INLINABLE totalizeRethrowingTo_ #-}
totalizeRethrowingTo_ :: ThreadId -> PartialHandler () -> TotalHandler ()
totalizeRethrowingTo_ t (PartialHandler h) =
  \e -> fromMaybe (throwTo t e) (h e)


-- * Standard handlers
-------------------------

-- |
-- A handler of exceptions of a specific type.
{-# INLINABLE typed #-}
typed :: Exception e => (e -> Maybe (IO a)) -> PartialHandler a
typed h =
  PartialHandler $ fromException >=> h
  

-- ** AsyncException handlers
-------------------------

-- |
-- A handler of the 'ThreadKilled' exception.
{-# INLINABLE onThreadKilled #-}
onThreadKilled :: IO a -> PartialHandler a
onThreadKilled handler =
  typed $ \case
    ThreadKilled -> Just handler
    _ -> Nothing

-- ** IOError handlers
-------------------------

-- |
-- A handler of all exceptions of type 'IOError' by their type.
{-# INLINABLE onIOErrorByType #-}
onIOErrorByType :: (IOErrorType -> Maybe (IO a)) -> PartialHandler a
onIOErrorByType handler =
  typed $ handler . ioeGetErrorType

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isAlreadyExistsError' predicate.
{-# INLINABLE onAlreadyExists #-}
onAlreadyExists :: IO a -> PartialHandler a
onAlreadyExists handler =
  typed $ \e -> if isAlreadyExistsError e then Just handler else Nothing

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isDoesNotExistError' predicate.
{-# INLINABLE onDoesNotExist #-}
onDoesNotExist :: IO a -> PartialHandler a
onDoesNotExist handler =
  typed $ \e -> if isDoesNotExistError e then Just handler else Nothing

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isAlreadyInUseError' predicate.
{-# INLINABLE onAlreadyInUse #-}
onAlreadyInUse :: IO a -> PartialHandler a
onAlreadyInUse handler =
  typed $ \e -> if isAlreadyInUseError e then Just handler else Nothing

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isFullError' predicate.
{-# INLINABLE onFull #-}
onFull :: IO a -> PartialHandler a
onFull handler =
  typed $ \e -> if isFullError e then Just handler else Nothing

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isEOFError' predicate.
{-# INLINABLE onEOF #-}
onEOF :: IO a -> PartialHandler a
onEOF handler =
  typed $ \e -> if isEOFError e then Just handler else Nothing

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isIllegalOperation' predicate.
{-# INLINABLE onIllegalOperation #-}
onIllegalOperation :: IO a -> PartialHandler a
onIllegalOperation handler =
  typed $ \e -> if isIllegalOperation e then Just handler else Nothing

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isPermissionError' predicate.
{-# INLINABLE onPermission #-}
onPermission :: IO a -> PartialHandler a
onPermission handler =
  typed $ \e -> if isPermissionError e then Just handler else Nothing

-- |
-- A handler of an 'IOError' exception 
-- with type satisfying the 'isUserError' predicate.
{-# INLINABLE onUser #-}
onUser :: IO a -> PartialHandler a
onUser handler =
  typed $ \e -> if isUserError e then Just handler else Nothing