{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
-- Description : Miscellaneous Monads, in particular 'Computation.WithError'.
module Util.Computation (
        Answer,

        done,

        ( # ), -- reverse of application

        -- * exceptions and handlers
        propagate,
        try, -- re-export from Control.Exception
        tryUntilOK,
        raise,

        -- * selectors
        when, -- re-export from Control.Monad
        unless, -- re-export from Control.Monad
        incase,

        -- * iterators
        forever, -- re-export from Control.Monad
        foreverUntil,
        foreach,
        while,

        -- * configure command
        Config,
        configure,
        config,

        -- * The new-style configuration command
        HasConfig(..),

        -- * Returning results or error messages.
        WithError,

        hasError, -- :: String -> WithError a
        -- pass on an error

        hasValue, -- :: a -> WithError a
        -- pass on a value

        fromWithError, -- :: WithError a -> Either String a
        -- unpack a WithError
        fromWithError1, -- :: a -> WithError a -> a
        -- simpler form.
        toWithError, -- :: Either String a -> WithError a
        -- pack a WithError
        isError, -- :: WithError a -> Bool
        -- returns True if this value indicates an error.

        mapWithError, -- :: (a -> b) -> WithError a -> WithError b
        mapWithError', -- :: (a -> WithError b) -> WithError a -> WithError b
        mapWithErrorIO,
        -- :: (a -> IO b) -> WithError a -> IO (WithError b)
        mapWithErrorIO',
        -- :: (a -> IO (WithError b)) -> WithError a -> IO (WithError b)
        pairWithError, -- :: WithError a -> WithError b -> WithError (a,b)
        -- we concatenate the errors, inserting a newline between them if
        -- there are two.
        listWithError, -- :: [WithError a] -> WithError [a]
        coerceWithError, -- :: WithError a -> a
        coerceWithErrorIO, -- :: WithError a -> IO a
        -- get out result or throw error.
        -- The second throws the error immediately.
        coerceWithErrorStringIO, -- :: String -> WithError a -> IO a
        -- Like coerceWithErrorIO but also takes a String, which will
        -- be included in the eventual error message.

        coerceWithErrorOrBreakIOPrefix,
           -- :: String -> (String -> a) -> WithError a -> IO a
        coerceWithErrorOrBreakPrefix,
           -- :: String -> (String -> a) -> WithError a -> a

        MonadWithError(..),
        -- newtype which wraps a monadic action returning a WithError a.
        -- This is itself an instance of Monad, allowing functions defined
        -- on monads, such as mapM, work on them.
        monadifyWithError, -- :: Monad m => WithError a -> MonadWithError m a
        toMonadWithError, -- :: Monad m => m a -> MonadWithError m a

        coerceWithErrorOrBreak, -- :: (String -> a) -> WithError a -> a
        -- coerce or use the supplied break function (to be used with
        -- ExtendedPrelude.addFallOut)

        coerceWithErrorOrBreakIO, -- :: (String -> a) -> WithError a -> IO a
        -- coerce or use the supplied break function (to be used with
        -- ExtendedPrelude.addFallOut)
        -- The value is evaluated immediately.

        concatWithError, -- :: [WithError a] -> WithError [a]
        -- like pair but using lists.

        swapIOWithError, -- :: WithError (IO a) -> IO (WithError a)
        -- Intended for use on result of mapWithError, for example.

        exceptionToError,
        -- :: (Exception -> Maybe String) -> IO a -> IO (WithError a)
        -- Exception wrapper that turns those exceptions which map to
        -- (Just message) into an error.
        )
where

import Control.Monad

import Control.Exception

import Util.Debug(debug)

infixr 2 #


-- --------------------------------------------------------------------------
-- Type Definitions
-- --------------------------------------------------------------------------

type Answer a = Either SomeException a

-- --------------------------------------------------------------------------
-- Done
-- --------------------------------------------------------------------------

done :: Monad m => m ()
done = return ()


-- --------------------------------------------------------------------------
-- Method Application
-- --------------------------------------------------------------------------

( # ) :: a -> (a -> b) -> b
o # f = f o


-- --------------------------------------------------------------------------
-- IOError and Exception Handling
-- --------------------------------------------------------------------------

raise :: IOError -> IO a
raise e =
   do
      debug ("RAISED EXCP: " ++ (show e) ++ "\n")
      ioError e

propagate :: Answer a -> IO a
propagate (Left e) = throw e
propagate (Right v) = return v

catchall :: IO a -> IO a -> IO a
catchall c1 c2 = Control.Exception.catch c1 (\ (_ :: SomeException) -> c2)

tryUntilOK :: IO a -> IO a
tryUntilOK c = catchall c (tryUntilOK c)

-- --------------------------------------------------------------------------
-- Values paired with error messages
-- --------------------------------------------------------------------------

data WithError a =
      Error String
   |  Value a -- error or result

hasError :: String -> WithError a
hasError str = Error str

hasValue :: a -> WithError a
hasValue a = Value a

toWithError :: Either String a -> WithError a
toWithError (Left s) = Error s
toWithError (Right a) = Value a

isError :: WithError a -> Bool
isError (Error _) = True
isError (Value _) = False

fromWithError :: WithError a -> Either String a
fromWithError (Error s) = Left s
fromWithError (Value a) = Right a

fromWithError1 :: a -> WithError a -> a
fromWithError1 _ (Value a) = a
fromWithError1 a (Error _) = a

mapWithError :: (a -> b) -> WithError a -> WithError b
mapWithError f (Error e) = Error e
mapWithError f (Value x) = Value (f x)

mapWithError' :: (a -> WithError b) -> WithError a -> WithError b
mapWithError' f (Error e) = Error e
mapWithError' f (Value a) = f a


mapWithErrorIO :: (a -> IO b) -> WithError a -> IO (WithError b)
mapWithErrorIO f (Error e) = return (Error e)
mapWithErrorIO f (Value a) =
   do
      b <- f a
      return (Value b)

mapWithErrorIO' :: (a -> IO (WithError b)) -> WithError a -> IO (WithError b)
mapWithErrorIO' f (Error e) = return (Error e)
mapWithErrorIO' f (Value a) = f a

pairWithError :: WithError a -> WithError b -> WithError (a,b)
-- we concatenate the errors, inserting a newline between them if there are two.
pairWithError (Value a) (Value b) = Value (a,b)
pairWithError (Error e) (Value b) = Error e
pairWithError (Value a) (Error f) = Error f
pairWithError (Error e) (Error f) = Error (e++"\n"++f)

listWithError :: [WithError a] -> WithError [a]
listWithError awes =
   foldr
      (\ awe awes ->
         mapWithError
            (\ (a,as) -> a:as)
            (pairWithError awe awes)
         )
      (hasValue [])
      awes

-- coerce or raise error
coerceWithError :: WithError a -> a
coerceWithError (Value a) = a
coerceWithError (Error err) = error err

coerceWithErrorIO :: WithError a -> IO a
coerceWithErrorIO (Value a) = return a
coerceWithErrorIO (Error err) = error err

coerceWithErrorStringIO :: String -> WithError a -> IO a
coerceWithErrorStringIO _ (Value a) = return a
coerceWithErrorStringIO mess (Error err) =
   error ("coerceWithErrorString " ++ mess ++ ": " ++ err)

-- | coerce or use the supplied break function (to be used with
-- 'ExtendedPrelude.addFallOut')
-- The value is evaluated immediately.
coerceWithErrorOrBreakIO :: (String -> a) -> WithError a -> IO a
coerceWithErrorOrBreakIO = coerceWithErrorOrBreakIOPrefix ""

-- | coerce or use the supplied break function (to be used with
-- 'ExtendedPrelude.addFallOut')
--
-- The first argument is prepended to any error message.
-- The value is evaluated immediately.
coerceWithErrorOrBreakIOPrefix
   :: String -> (String -> a) -> WithError a -> IO a
coerceWithErrorOrBreakIOPrefix errorPrefix breakFn aWe =
   do
      let
         a = coerceWithErrorOrBreakPrefix errorPrefix breakFn aWe
      seq a (return a)

-- | coerce or use the supplied break function (to be used with
-- 'ExtendedPrelude.addFallOut')
coerceWithErrorOrBreak :: (String -> a) -> WithError a -> a
coerceWithErrorOrBreak = coerceWithErrorOrBreakPrefix ""


-- | coerce or use the supplied break function (to be used with
-- 'ExtendedPrelude.addFallOut')
--
-- The first argument is prepended to any error message.
coerceWithErrorOrBreakPrefix :: String -> (String -> a) -> WithError a -> a
coerceWithErrorOrBreakPrefix errorPrefix breakFn (Value a) = a
coerceWithErrorOrBreakPrefix errorPrefix breakFn (Error s)
   = breakFn (errorPrefix ++ s)

concatWithError :: [WithError a] -> WithError [a]
concatWithError withErrors =
   foldr
      (\ wE wEsf -> mapWithError (uncurry (:)) (pairWithError wE wEsf))
      (Value [])
      withErrors

swapIOWithError :: WithError (IO a) -> IO (WithError a)
swapIOWithError (Error e) = return (Error e)
swapIOWithError (Value act) =
   do
      v <- act
      return (Value v)

exceptionToError :: Exception e => (e -> Maybe String) -> IO a -> IO (WithError a)
exceptionToError testFn action =
   catchJust
      testFn
      (do
          val <- action
          return (hasValue val)
      )
      (\ str -> return (hasError str))

instance Functor WithError where
   fmap aToB aWE = case aWE of
      Value a -> Value (aToB a)
      Error e -> Error e

instance Monad WithError where
   return v = hasValue v
   (>>=) aWE toBWe =
      mapWithError' toBWe aWE
   fail s = hasError s

newtype MonadWithError m a = MonadWithError (m (WithError a))

instance Monad m => Monad (MonadWithError m) where
   return v = MonadWithError (return (Value v))
   (>>=) (MonadWithError act1) getAct2 =
      MonadWithError (
         do
            valWithError <- act1
            case valWithError of
               Value v ->
                  let
                     (MonadWithError act2) = getAct2 v
                  in
                     act2
               Error s -> return (Error s)
         )
   fail s = MonadWithError (return (Error s))

monadifyWithError :: Monad m => WithError a -> MonadWithError m a
monadifyWithError we = MonadWithError (return we)

toMonadWithError :: Monad m => m a -> MonadWithError m a
toMonadWithError act = MonadWithError (
   do
      a <- act
      return (hasValue a)
   )

-- --------------------------------------------------------------------------
-- Derived Control Abstractions: Iteration
-- --------------------------------------------------------------------------

foreverUntil :: Monad m => m Bool -> m ()
foreverUntil act =
   do
      stop <- act
      if stop
         then
            done
         else
            foreverUntil act

foreach :: Monad m => [a] -> (a -> m b) -> m ()
foreach el c = sequence_ (map c el)   -- mapM c el

-- --------------------------------------------------------------------------
-- Derived Control Abstractions: Selection
-- --------------------------------------------------------------------------

incase :: Maybe a -> (a -> IO b) -> IO ()
incase Nothing f = done
incase (Just a) f = do {f a; done}

-- --------------------------------------------------------------------------
-- Loops
-- --------------------------------------------------------------------------

while :: Monad m => m a -> (a -> Bool) -> m a
while c p = c >>= \x -> if (p x) then while c p else return x


-- --------------------------------------------------------------------------
-- Configuration Options
-- --------------------------------------------------------------------------

type Config w = w -> IO w

configure :: w -> [Config w] -> IO w
configure w [] = return w
configure w (c:cl) = do {w' <- c w; configure w' cl}

config :: IO () -> Config w
config f w = f >> return w


-- --------------------------------------------------------------------------
-- New-style configuration
-- Where HasConfig is defined you can type
--     option1  $$ option2 $$ ... $$ initial_configuration
-- --------------------------------------------------------------------------

class HasConfig option configuration where
   ($$) :: option -> configuration -> configuration

   configUsed :: option -> configuration -> Bool
   -- In some implementations (EG a text-only
   -- implementation of the GraphDisp interface)
   -- we may create default configurations in which $$ simply
   -- ignores the option.  In such cases configUsed should return
   -- False.

infixr 0 $$
-- This makes $$ have fixity like $.