{-# LANGUAGE ScopedTypeVariables #-}

module Control.Error.Context.Exception where

import           Control.Error.Context.Types

import           Control.Exception.Safe      (SomeException (..), catchJust)
import           Control.Monad
import           Control.Monad.Catch         (Exception (..), MonadCatch (..))

-- | Like 'catch', but the handler is required to be context-aware. Is
-- also able to catch exceptions of type 'e' (without context).
catchWithContext
  :: (MonadCatch m, Exception e)
  => m a
  -> (ErrorWithContext e -> m a)
  -> m a
catchWithContext m handler = catchJust pre m handler
  where pre :: Exception e => SomeException -> Maybe (ErrorWithContext e)
        pre someExn =
          -- First we check if the exception is of the type
          -- 'ErrorWithContext e'. If so, provide it to the handler
          -- directly.
          case fromException someExn of
            Just (ErrorWithContext ctx someExnWithoutCtx :: ErrorWithContext SomeException) ->
              case fromException someExnWithoutCtx of
                Just exn -> Just (ErrorWithContext ctx exn)
                Nothing  -> Nothing
            Nothing  ->
              -- Then we check if the exception is of the type 'e',
              -- (without context). In this case we convert it into an
              -- 'ErrorWithContext e' by adding an empty context and
              -- provide the wrapped exception with context to the
              -- handler.
              case fromException someExn of
                Just exn ->
                  Just (ErrorWithContext mempty exn)
                Nothing ->
                  Nothing

-- | Like 'catch', but the handler is required to be context-unaware.
-- Is also able to catch exceptions with context, in which case the
-- context will be forgotten before the exception will be provided to
-- the handler.
catchWithoutContext
  :: forall a e m
   . (MonadCatch m, Exception e)
  => m a
  -> (e -> m a)
  -> m a
catchWithoutContext m handler = catchJust pre m handler
  where pre :: SomeException -> Maybe e
        pre someExn =
          -- First we check if the exception is of the type
          -- 'ErrorWithContext e'. In this case we forget the context
          -- and provide the exception without context to the handler.
          case fromException someExn :: Maybe (ErrorWithContext SomeException) of
            Just (ErrorWithContext _ctx someExnWithoutContext) ->
              case fromException someExnWithoutContext :: Maybe e of
                Just exn ->
                  Just exn
                Nothing ->
                  Nothing
            Nothing  ->
              -- Then we check if the exception is of type 'e'. If so,
              -- provide it to the handler directly.
              case fromException someExn :: Maybe e of
                Just exn ->
                  Just exn
                Nothing  ->
                  Nothing

tryAnyWithContext
  :: MonadCatch m
  => m a
  -> m (Either (ErrorWithContext SomeException) a)
tryAnyWithContext m =
  catchWithContext (Right `liftM` m) (return . Left)

tryAnyWithoutContext
  :: MonadCatch m
  => m a
  -> m (Either SomeException a)
tryAnyWithoutContext m =
  catchWithoutContext (Right `liftM` m) (return . Left)

tryWithContext
  :: (MonadCatch m, Exception e)
  => m a
  -> m (Either (ErrorWithContext e) a)
tryWithContext m =
  catchWithContext (Right `liftM` m) (return . Left)

tryWithoutContext
  :: (MonadCatch m, Exception e)
  => m a
  -> m (Either e a)
tryWithoutContext m =
  catchWithoutContext (Right `liftM` m) (return . Left)

-- | Forgets the context from an enriched error.
errorContextForget
  :: ErrorWithContext e
  -> e
errorContextForget (ErrorWithContext _ctx e) = e

-- | Context aware version of 'catchAny'.
catchAnyWithContext
  :: MonadCatch m
  => m a
  -> (ErrorWithContext SomeException -> m a)
  -> m a
catchAnyWithContext m handler = catchJust pre m handler
  where pre :: SomeException -> Maybe (ErrorWithContext SomeException)
        pre someExn =
          case fromException someExn :: Maybe (ErrorWithContext SomeException) of
            Just exn ->
              Just exn
            Nothing ->
              Just (ErrorWithContext mempty someExn)

-- | Context aware version of 'catchAny'.
catchAnyWithoutContext
  :: MonadCatch m
  => m a
  -> (SomeException -> m a)
  -> m a
catchAnyWithoutContext m handler = catchJust pre m handler
  where pre :: SomeException -> Maybe SomeException
        pre someExn =
          case fromException someExn :: Maybe (ErrorWithContext SomeException) of
            Just (ErrorWithContext _ctx exnWithoutContext) ->
              Just exnWithoutContext
            Nothing ->
              Just someExn