{-# LANGUAGE CPP #-}
#ifdef __GLASGOW_HASKELL__
{-# LANGUAGE DeriveDataTypeable #-}
#endif
-- | When you've caught all the exceptions that can be handled safely,
--   this is what you're left with.
--
-- > runEitherIO . fromIO ≡ id
--
-- It is intended that you use qualified imports with this library.
--
-- > import UnexceptionalIO (UIO)
-- > import qualified UnexceptionalIO as UIO
module UnexceptionalIO (
        UIO,
        Unexceptional(..),
        fromIO,
#ifdef __GLASGOW_HASKELL__
        fromIO',
#endif
        run,
        runEitherIO,
        -- * Unsafe entry points
        unsafeFromIO,
        -- * Pseudo exceptions
        SomeNonPseudoException,
#ifdef __GLASGOW_HASKELL__
        PseudoException(..),
        ProgrammerError(..),
        ExternalError(..),
        -- * Pseudo exception helpers
        bracket,
#if MIN_VERSION_base(4,7,0)
        forkFinally,
        fork,
        ChildThreadError(..)
#endif
#endif
) where

import Data.Maybe (fromMaybe)
import Control.Applicative (Applicative(..), (<|>), (<$>))
import Control.Monad (liftM, ap, (<=<))
import Control.Monad.Fix (MonadFix(..))
#ifdef __GLASGOW_HASKELL__
import System.Exit (ExitCode)
import Control.Exception (try)
import Data.Typeable (Typeable)
import qualified Control.Exception as Ex
import qualified Control.Concurrent as Concurrent
#if MIN_VERSION_base(4,11,0)
import qualified Control.Exception.Base as Ex
#endif

-- | Not everything handled by the exception system is a run-time error
-- you can handle.  This is the class of unrecoverable pseudo-exceptions.
--
-- Additionally, except for 'ExitCode' any of these pseudo-exceptions
-- you could never guarantee to have caught.  Since they can come
-- from anywhere at any time, we could never guarentee that 'UIO' does
-- not contain them.
data PseudoException =
        ProgrammerError ProgrammerError | -- ^ Mistakes programmers make
        ExternalError   ExternalError   | -- ^ Errors thrown by the runtime
        Exit ExitCode                     -- ^ Process exit requests
        deriving (Show, Typeable)

instance Ex.Exception PseudoException where
        toException (ProgrammerError e) = Ex.toException e
        toException (ExternalError e)   = Ex.toException e
        toException (Exit e)            = Ex.toException e

        fromException e =
                ProgrammerError <$> Ex.fromException e <|>
                ExternalError   <$> Ex.fromException e <|>
                Exit            <$> Ex.fromException e

-- | Pseudo-exceptions caused by a programming error
--
-- Partial functions, 'error', 'undefined', etc
data ProgrammerError =
#if MIN_VERSION_base(4,9,0)
        TypeError Ex.TypeError               |
#endif
        ArithException Ex.ArithException     |
        ArrayException Ex.ArrayException     |
        AssertionFailed Ex.AssertionFailed   |
        ErrorCall Ex.ErrorCall               |
        NestedAtomically Ex.NestedAtomically |
        NoMethodError Ex.NoMethodError       |
        PatternMatchFail Ex.PatternMatchFail |
        RecConError Ex.RecConError           |
        RecSelError Ex.RecSelError           |
        RecUpdError Ex.RecSelError
        deriving (Show, Typeable)

instance Ex.Exception ProgrammerError where
#if MIN_VERSION_base(4,9,0)
        toException (TypeError e)           = Ex.toException e
#endif
        toException (ArithException e)      = Ex.toException e
        toException (ArrayException e)      = Ex.toException e
        toException (AssertionFailed e)     = Ex.toException e
        toException (ErrorCall e)           = Ex.toException e
        toException (NestedAtomically e)    = Ex.toException e
        toException (NoMethodError e)       = Ex.toException e
        toException (PatternMatchFail e)    = Ex.toException e
        toException (RecConError e)         = Ex.toException e
        toException (RecSelError e)         = Ex.toException e
        toException (RecUpdError e)         = Ex.toException e

        fromException e =
#if MIN_VERSION_base(4,9,0)
                TypeError        <$> Ex.fromException e <|>
#endif
                ArithException   <$> Ex.fromException e <|>
                ArrayException   <$> Ex.fromException e <|>
                AssertionFailed  <$> Ex.fromException e <|>
                ErrorCall        <$> Ex.fromException e <|>
                NestedAtomically <$> Ex.fromException e <|>
                NoMethodError    <$> Ex.fromException e <|>
                PatternMatchFail <$> Ex.fromException e <|>
                RecConError      <$> Ex.fromException e <|>
                RecSelError      <$> Ex.fromException e <|>
                RecUpdError      <$> Ex.fromException e

-- | Pseudo-exceptions thrown by the runtime environment
data ExternalError =
#if MIN_VERSION_base(4,10,0)
        CompactionFailed Ex.CompactionFailed                   |
#endif
#if MIN_VERSION_base(4,11,0)
        FixIOException Ex.FixIOException                       |
#endif
#if MIN_VERSION_base(4,7,0)
        AsyncException Ex.SomeAsyncException                   |
#else
        AsyncException Ex.AsyncException                       |
#endif
        BlockedIndefinitelyOnSTM Ex.BlockedIndefinitelyOnSTM   |
        BlockedIndefinitelyOnMVar Ex.BlockedIndefinitelyOnMVar |
        Deadlock Ex.Deadlock                                   |
        NonTermination Ex.NonTermination
        deriving (Show, Typeable)

instance Ex.Exception ExternalError where
#if MIN_VERSION_base(4,10,0)
        toException (CompactionFailed e)          = Ex.toException e
#endif
#if MIN_VERSION_base(4,11,0)
        toException (FixIOException e)            = Ex.toException e
#endif
        toException (AsyncException e)            = Ex.toException e
        toException (BlockedIndefinitelyOnMVar e) = Ex.toException e
        toException (BlockedIndefinitelyOnSTM e)  = Ex.toException e
        toException (Deadlock e)                  = Ex.toException e
        toException (NonTermination e)            = Ex.toException e

        fromException e =
#if MIN_VERSION_base(4,10,0)
                CompactionFailed          <$> Ex.fromException e <|>
#endif
#if MIN_VERSION_base(4,11,0)
                FixIOException            <$> Ex.fromException e <|>
#endif
                AsyncException            <$> Ex.fromException e <|>
                BlockedIndefinitelyOnSTM  <$> Ex.fromException e <|>
                BlockedIndefinitelyOnMVar <$> Ex.fromException e <|>
                Deadlock                  <$> Ex.fromException e <|>
                NonTermination            <$> Ex.fromException e

-- | Every 'Ex.SomeException' but 'PseudoException'
newtype SomeNonPseudoException = SomeNonPseudoException Ex.SomeException deriving (Show, Typeable)

instance Ex.Exception SomeNonPseudoException where
        toException (SomeNonPseudoException e) = e

        fromException e = case Ex.fromException e of
                Just pseudo -> const Nothing (pseudo :: PseudoException)
                Nothing -> Just (SomeNonPseudoException e)

throwIO :: (Ex.Exception e) => e -> IO a
throwIO = Ex.throwIO
#else
-- Haskell98 import 'IO' instead
import System.IO.Error (IOError, ioError, try)

type SomeNonPseudoException = IOError

throwIO :: SomeNonPseudoException -> IO a
throwIO = ioError
#endif

-- | Like IO, but throws only 'PseudoException'
newtype UIO a = UIO (IO a)

instance Functor UIO where
        fmap = liftM

instance Applicative UIO where
        pure = return
        (<*>) = ap

instance Monad UIO where
        return = UIO . return
        (UIO x) >>= f = UIO (x >>= run . f)
#if !MIN_VERSION_base(4,13,0)
        fail s = error $ "UnexceptionalIO cannot fail (" ++ s ++ ")"
#endif

instance MonadFix UIO where
        mfix f = UIO (mfix $ run . f)

-- | Monads in which 'UIO' computations may be embedded
class (Monad m) => Unexceptional m where
        lift :: UIO a -> m a

instance Unexceptional UIO where
        lift = id

instance Unexceptional IO where
        lift = run

-- | Catch any exception but 'PseudoException' in an 'IO' action
fromIO :: (Unexceptional m) => IO a -> m (Either SomeNonPseudoException a)
fromIO = unsafeFromIO . try

#ifdef __GLASGOW_HASKELL__
-- | Catch any 'e' in an 'IO' action, with a default mapping for
--   unexpected cases
fromIO' :: (Ex.Exception e, Unexceptional m) =>
        (SomeNonPseudoException -> e) -- ^ Default if an unexpected exception occurs
        -> IO a
        -> m (Either e a)
fromIO' f = liftM (either (Left . f) id) . fromIO . try
#endif

-- | Re-embed 'UIO' into 'IO'
run :: UIO a -> IO a
run (UIO io) = io

-- | Re-embed 'UIO' and possible exception back into 'IO'
#ifdef __GLASGOW_HASKELL__
runEitherIO :: (Ex.Exception e) => UIO (Either e a) -> IO a
#else
runEitherIO :: UIO (Either SomeNonPseudoException a) -> IO a
#endif
runEitherIO = either throwIO return <=< run

-- | You promise there are no exceptions but 'PseudoException' thrown by this 'IO' action
unsafeFromIO :: (Unexceptional m) => IO a -> m a
unsafeFromIO = lift . UIO

#ifdef __GLASGOW_HASKELL__
-- | When you're doing resource handling, 'PseudoException' matters.
--   You still need to use the 'Ex.bracket' pattern to handle cleanup.
bracket :: (Unexceptional m) => UIO a -> (a -> UIO ()) -> (a -> UIO c) -> m c
bracket acquire release body =
        unsafeFromIO $ Ex.bracket (run acquire) (run . release) (run . body)

#if MIN_VERSION_base(4,7,0)
-- | Mirrors 'Concurrent.forkFinally', but since the body is 'UIO',
--   the thread must terminate successfully or because of 'PseudoException'
forkFinally :: (Unexceptional m) => UIO a -> (Either PseudoException a -> UIO ()) -> m Concurrent.ThreadId
forkFinally body handler = unsafeFromIO $ Concurrent.forkFinally (run body) $ \result ->
        case result of
                Left e -> case Ex.fromException e of
                        Just pseudo -> run $ handler $ Left pseudo
                        Nothing -> error $ "Bug in UnexceptionalIO: forkFinally caught a non-PseudoException: " ++ show e
                Right x -> run $ handler $ Right x

-- | Mirrors 'Concurrent.forkIO', but re-throws errors to the parent thread
--
-- * Ignores manual thread kills, since those are on purpose.
-- * Re-throws async exceptions ('SomeAsyncException') as is.
-- * Re-throws 'ExitCode' as is in an attempt to exit with the requested code.
-- * Wraps synchronous 'PseudoException' in async 'ChildThreadError'.
fork :: (Unexceptional m) => UIO () -> m Concurrent.ThreadId
fork body = do
        parent <- unsafeFromIO Concurrent.myThreadId
        forkFinally body $ either (handler parent) (const $ return ())
        where
        handler parent e
                -- Thread manually killed. I assume on purpose
                | Just Ex.ThreadKilled <- castException e = return ()
                -- Async exception, nothing to do with this thread, propogate directly
                | Just (Ex.SomeAsyncException _) <- castException e =
                        unsafeFromIO $ Concurrent.throwTo parent e
                -- Attempt to manually end the process,
                -- not an async exception, so a bit dangerous to throw async'ly, but
                -- you really do want this to reach the top as-is for the exit code to
                -- work.
                | Just e <- castException e =
                        unsafeFromIO $ Concurrent.throwTo parent (e :: ExitCode)
                -- Non-async PseudoException, so wrap in an async wrapper before
                -- throwing async'ly
                | otherwise = unsafeFromIO $ Concurrent.throwTo parent (ChildThreadError e)

castException :: (Ex.Exception e1, Ex.Exception e2) => e1 -> Maybe e2
castException = Ex.fromException . Ex.toException

-- | Async signal that a child thread ended due to non-async PseudoException
newtype ChildThreadError = ChildThreadError PseudoException deriving (Show, Typeable)

instance Ex.Exception ChildThreadError where
        toException = Ex.asyncExceptionToException
        fromException = Ex.asyncExceptionFromException
#endif
#endif