{-# LANGUAGE ExistentialQuantification, ScopedTypeVariables #-}
module Control.Monad.CatchIO
  (
    MonadCatchIO(..)
  , E.Exception(..)
  , throw
  , try, tryJust
  , Handler(..), catches
  -- * Utilities
  , bracket
  , bracket_
  , bracketOnError
  , finally
  , onException
  )

where

import           Prelude hiding ( catch )
import           Control.Applicative                          ((<$>))
import qualified Control.Exception.Extensible      as E

import           Control.Monad.IO.Class                       (MonadIO,liftIO)

import           Control.Monad.Trans.Cont                     (ContT(ContT)    ,runContT    ,mapContT    )
import           Control.Monad.Trans.Error                    (ErrorT          ,runErrorT   ,mapErrorT   ,Error)
import           Control.Monad.Trans.Identity                 (IdentityT       ,runIdentityT,mapIdentityT)
import           Control.Monad.Trans.List                     (ListT(ListT)    ,runListT    ,mapListT    )
import           Control.Monad.Trans.Maybe                    (MaybeT          ,runMaybeT   ,mapMaybeT   )
import           Control.Monad.Trans.RWS                      (RWST(RWST)      ,runRWST     ,mapRWST     )
import qualified Control.Monad.Trans.RWS.Strict    as Strict  (RWST(RWST)      ,runRWST     ,mapRWST     )
import           Control.Monad.Trans.Reader                   (ReaderT(ReaderT),runReaderT  ,mapReaderT  )
import           Control.Monad.Trans.State                    (StateT(StateT)  ,runStateT   ,mapStateT   )
import qualified Control.Monad.Trans.State.Strict  as Strict  (StateT(StateT)  ,runStateT   ,mapStateT   )
import           Control.Monad.Trans.Writer                   (WriterT         ,runWriterT  ,mapWriterT  )
import qualified Control.Monad.Trans.Writer.Strict as Strict  (WriterT         ,runWriterT  ,mapWriterT  )

import           Data.Monoid                                  (Monoid)


class MonadIO m => MonadCatchIO m where
  -- | Generalized version of 'E.catch'
  catch   :: E.Exception e => m a -> (e -> m a) -> m a
  
  -- | Generalized version of 'E.block'
  block   :: m a -> m a
  
  -- | Generalized version of 'E.unblock'
  unblock :: m a -> m a


instance MonadCatchIO IO where
  catch   = E.catch
  block   = E.block
  unblock = E.unblock

-- | Warning: this instance is somewhat contentious.
-- 
-- In the same way that the @ErrorT e@ instance may fail to perform the final
-- action, due to the \"early exit\" behaviour of the monad, this instance
-- may perform the final action any number of times, due to the nonlinear
-- nature of the continuation monad.
-- 
-- See the mailing list message
-- <http://web.archiveorange.com/archive/v/nDNOvaYx1poDHZNlmlgh>
-- for an example of what can go wrong (freeing memory twice).
instance MonadCatchIO m => MonadCatchIO (ContT r m) where
  m `catch` f = ContT $ \c -> runContT m c `catch` \e -> runContT (f e) c
  block       = mapContT block
  unblock     = mapContT unblock

-- | Warning: this instance is somewhat contentious.
-- 
-- Note that in monads that fall under this instance (the most basic example
-- is @ErrorT e IO@), there are errors of two sorts:
-- 
-- 1. exceptions, (i.e., exceptional values in the underlying @IO@ monad);
-- 
-- 2. error values of type @e@, introduced by the @ErrorT e@ part of the monad.
-- 
-- The instance takes no special action to deal with errors of type 2.
-- In particular, 'bracket' will not perform its second argument, if
-- its third argument decides to \"exit early\" by throwing an error of type 2.
-- 
-- This may or may not be what you want.
-- 
-- See the mailing list thread starting with
-- <http://www.mail-archive.com/haskell-cafe@haskell.org/msg82859.html>
-- for some details.
instance (MonadCatchIO m, Error e) => MonadCatchIO (ErrorT e m) where
  m `catch` f = mapErrorT (\m' -> m' `catch` \e -> runErrorT $ f e) m
  block       = mapErrorT block
  unblock     = mapErrorT unblock

instance (MonadCatchIO m) => MonadCatchIO (IdentityT m) where
  m `catch` f = mapIdentityT (\m' -> m' `catch` \e -> runIdentityT $ f e) m
  block       = mapIdentityT block
  unblock     = mapIdentityT unblock

instance MonadCatchIO m => MonadCatchIO (ListT m) where
  m `catch` f = ListT $ runListT m `catch` \e -> runListT (f e)
  block       = mapListT block
  unblock     = mapListT unblock

instance (MonadCatchIO m) => MonadCatchIO (MaybeT m) where
  m `catch` f = mapMaybeT (\m' -> m' `catch` \e -> runMaybeT $ f e) m
  block       = mapMaybeT block
  unblock     = mapMaybeT unblock

instance (Monoid w, MonadCatchIO m) => MonadCatchIO (RWST r w s m) where
  m `catch` f = RWST $ \r s -> runRWST m r s `catch` \e -> runRWST (f e) r s
  block       = mapRWST block
  unblock     = mapRWST unblock

instance (Monoid w, MonadCatchIO m) => MonadCatchIO (Strict.RWST r w s m) where
  m `catch` f = Strict.RWST $ \r s -> Strict.runRWST m r s `catch` \e -> Strict.runRWST (f e) r s
  block       = Strict.mapRWST block
  unblock     = Strict.mapRWST unblock

instance MonadCatchIO m => MonadCatchIO (ReaderT r m) where
  m `catch` f = ReaderT $ \r -> runReaderT m r `catch` \e -> runReaderT (f e) r
  block       = mapReaderT block
  unblock     = mapReaderT unblock

instance MonadCatchIO m => MonadCatchIO (StateT s m) where
  m `catch` f = StateT $ \s -> runStateT m s `catch` \e -> runStateT (f e) s
  block       = mapStateT block
  unblock     = mapStateT unblock

instance MonadCatchIO m => MonadCatchIO (Strict.StateT s m) where
  m `catch` f = Strict.StateT $ \s -> Strict.runStateT m s `catch` \e -> Strict.runStateT (f e) s
  block       = Strict.mapStateT block
  unblock     = Strict.mapStateT unblock

instance (Monoid w, MonadCatchIO m) => MonadCatchIO (WriterT w m) where
  m `catch` f = mapWriterT (\m' -> m' `catch` \e -> runWriterT $ f e) m
  block       = mapWriterT block
  unblock     = mapWriterT unblock

instance (Monoid w, MonadCatchIO m) => MonadCatchIO (Strict.WriterT w m) where
  m `catch` f = Strict.mapWriterT (\m' -> m' `catch` \e -> Strict.runWriterT $ f e) m
  block       = Strict.mapWriterT block
  unblock     = Strict.mapWriterT unblock

-- | Generalized version of 'E.throwIO'
throw :: (MonadIO m, E.Exception e) => e -> m a
throw = liftIO . E.throwIO

-- | Generalized version of 'E.try'
try :: (MonadCatchIO m, Functor m, E.Exception e) => m a -> m (Either e a)
try a = catch (Right <$> a) (return . Left)

-- | Generalized version of 'E.tryJust'
tryJust :: (MonadCatchIO m, Functor m, E.Exception e)
        => (e -> Maybe b) -> m a -> m (Either b a)
tryJust p a = do
  r <- try a
  case r of
    Right v -> return (Right v)
    Left  e -> case p e of
      Nothing -> throw e `asTypeOf` return (Left undefined)
      Just b  -> return (Left b)

-- | Generalized version of 'E.Handler'
data Handler m a = forall e . E.Exception e => Handler (e -> m a)

-- | Generalized version of 'E.catches'
catches :: MonadCatchIO m => m a -> [Handler m a] -> m a
catches a handlers = a `catch` handler where
  handler e = foldr tryH (throw e) handlers where
    tryH (Handler h) res = maybe res h $ E.fromException e

-- | Generalized version of 'E.bracket'
bracket :: MonadCatchIO m => m a -> (a -> m b) -> (a -> m c) -> m c
bracket before after thing = block $ do
  a <- before
  r <- unblock (thing a) `onException` after a
  _ <- after a
  return r

-- | Generalized version of 'E.onException'
onException :: MonadCatchIO m => m a -> m b -> m a
onException a onEx = a `catch` (\ (e :: E.SomeException) -> onEx >> throw e)

-- | A variant of 'bracket' where the return value from the first computation
-- is not required.
bracket_ :: MonadCatchIO m
         => m a  -- ^ computation to run first (\"acquire resource\")
         -> m b  -- ^ computation to run last (\"release resource\")
         -> m c  -- ^ computation to run in-between
         -> m c  -- returns the value from the in-between computation
bracket_ before after thing = block $ do
  _ <- before
  r <- unblock thing `onException` after
  _ <- after
  return r

-- | A specialised variant of 'bracket' with just a computation to run
-- afterward.
finally :: MonadCatchIO m
        => m a -- ^ computation to run first
        -> m b -- ^ computation to run afterward (even if an exception was
               -- raised)
        -> m a -- returns the value from the first computation
thing `finally` after = block $ do
  r <- unblock thing `onException` after
  _ <- after
  return r

-- | Like 'bracket', but only performs the final action if there was an
-- exception raised by the in-between computation.
bracketOnError :: MonadCatchIO m
               => m a        -- ^ computation to run first (\"acquire resource\")
               -> (a -> m b) -- ^ computation to run last (\"release resource\")
               -> (a -> m c) -- ^ computation to run in-between
               -> m c        -- returns the value from the in-between computation
bracketOnError before after thing = block $ do
  a <- before
  unblock (thing a) `onException` after a
