{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Module      : Control.Monad.Prompt
-- Description : Prompt monad and transformer
-- Copyright   : (c) Justin Le 2015
-- License     : MIT
-- Maintainer  : justin@jle.im
-- Stability   : unstable
-- Portability : portable
--
-- Provides the 'PromptT' type, which allows you to program computations
-- that can "ask" or "prompt" with values to get values in return.  The
-- computation doesn't care about the process of prompting, or how it
-- works, and has nothing to do with the effectful monad where the
-- prompting will eventually take place.
--
-- For example, sometimes you might want a computation to be able to query
-- or database, or talk with stdio, but you don't want your type to involve
-- arbitrary IO or be over IO, opening the door to a mess of IO.  'Prompt'
-- lets you write programs that can query "something", and then at a later
-- point in time, run it, providing the method of fulfilling each prompt.
-- Write your program independent of IO, or databases, or stdio, etc.; only
-- later "fill in" what it means.  You can even run the same 'Prompt' with
-- different ways to fulfill the prompts --- pure, effectful, etc.
--
-- For usage examples and a more detailed explanation, see
-- <https://github.com/mstksg/prompt the README>.

module Control.Monad.Prompt (
  -- * Prompt
    Prompt
  , runPromptM
  , runPrompt
  , interactP
  -- * PromptT
  , PromptT
  , runPromptTM
  , runPromptT
  , interactPT
  -- * Prompting
  , MonadPrompt(..)
  , prompt'
  , prompts'
  -- ** Specialized
  , promptP
  , promptsP
  , promptP'
  , promptsP'
  -- * Low level
  , mapPromptT
  , hoistP
  , liftP
  , mkPromptT
  , mkPrompt
  ) where

import Control.Applicative
import Control.Monad.Compat hiding (sequence, mapM, msum)
import Control.Monad.Error.Class
import Control.Monad.Prompt.Class
import Control.Monad.Reader.Class
import Control.Monad.State.Class
import Control.Monad.Trans
import Control.Monad.Writer.Class
import Data.Foldable
import Data.Functor.Identity
import Prelude.Compat


-- | Like 'Prompt', but can perform its "pure" computations in the context
-- of a 'Traversable' @t@, to absorb short-circuiting behvaior with 'Maybe'
-- or 'Either', logging with 'Writer', etc., but this is in general
-- completely unrelated to the effectful monad where the prompting will
-- eventually take place.  Specify short-circuiting and logging logic,
-- without worrying about IO or anything relating to the prompting effect.
--
-- @
-- 'prompt' :: a -> (PromptT a b t) b
-- @
--
-- Implements several useful typeclasses for working with the underlying
-- 'Traversable' and integrating effects, like 'Alternative', 'MonadError',
-- 'MonadWriter', etc.
--
-- Constructor is hidden, but a direct constructing function is exported as
-- 'mkPrompT' in the rare case it is needed or wanted.
--
newtype PromptT a b t r = PromptT (forall m. Monad m => (a -> m (t b)) -> m (t r))

-- | Prompt type, providing the ability to "prompt" or "query" by
-- presenting/asking with an @a@ and receiving a @b@ response.
--
-- @
-- 'prompt' :: a -> (Prompt a b) b
-- @
--
-- "Ask with an @a@, get a @b@."
--
-- Has a 'Monad', 'Applicative', 'Functor', etc. instance so it can be
-- sequenced monadically or applicatively, so you can sequence and bind
-- from 'prompt'.
--
-- Note that we defer the process of specifying /how/ 'prompt' delivers its
-- @b@.  It can take place in IO, or in any other effectful setting...but
-- 'Prompt' doesn't care, and it never involves IO or any arbitrary IO
-- itself.
--
-- Can be "constructed directly" using 'mkPrompt', but typically using
-- 'prompt' and the 'Applicative', 'Monad' instances etc. is better.
--
type Prompt a b = PromptT a b Identity

instance Functor t => Functor (PromptT a b t) where
#if MIN_VERSION_base(4,8,0)
    fmap f (PromptT p) = PromptT $ (fmap . fmap) f . p
#else
    fmap f (PromptT p) = PromptT $ (liftM . fmap) f . p
#endif

instance Applicative t => Applicative (PromptT a b t) where
    pure x = PromptT $ const (return (pure x))
#if MIN_VERSION_base(4,8,0)
    PromptT f <*> PromptT x = PromptT $ \g -> liftA2 (<*>) (f g) (x g)
#else
    PromptT f <*> PromptT x = PromptT $ \g -> liftM2 (<*>) (f g) (x g)
#endif

instance (Alternative t, Traversable t) => Alternative (PromptT a b t) where
    empty = PromptT $ const (return empty)
    PromptT x <|> PromptT y = PromptT $ \g -> do
        x' <- x g
        let c = (return . pure <$> x') <|> pure (y g)
#if MIN_VERSION_base(4,8,0)
        -- TODO: Is this okay?????
        -- join <$> sequence c
        asum <$> sequence c
#else
        asum `liftM` sequence c
#endif

instance (Monad t, Traversable t) => Monad (PromptT a b t) where
    return x = PromptT $ const (return (return x))
    PromptT p >>= f = PromptT $ \g -> do
#if MIN_VERSION_base(4,8,0)
        PromptT x <- traverse f <$> p g
        join <$> x g
#else
        PromptT x <- mapM f `liftM` p g
        join `liftM` x g
#endif

instance (MonadPlus t, Traversable t) => MonadPlus (PromptT a b t) where
#if MIN_VERSION_base(4,8,0)
    mzero = empty
    mplus = (<|>)
#else
    mzero = PromptT $ const (return mzero)
    PromptT x `mplus` PromptT y = PromptT $ \g -> do
        x' <- x g
        let c = (return . return <$> x') `mplus` return (y g)
        msum `liftM` sequence c
#endif

instance MonadTrans (PromptT a b) where
    lift x = PromptT $ const (return x)

instance (MonadError e t, Traversable t) => MonadError e (PromptT a b t) where
    throwError = lift . throwError
    catchError (PromptT p) f = PromptT $ \g -> do
      x <- p g
      let PromptT p' = sequence $ fmap return x `catchError` \e -> return (f e)
#if MIN_VERSION_base(4,8,0)
      join <$> p' g
#else
      join `liftM` p' g
#endif

instance (MonadReader r t, Traversable t) => MonadReader r (PromptT a b t) where
    ask = lift ask
    reader = lift . reader
    local = mapPromptT . local

instance (MonadState s t, Traversable t) => MonadState s (PromptT a b t) where
    get = lift get
    put = lift . put
    state = lift . state

instance (MonadWriter w t, Traversable t) => MonadWriter w (PromptT a b t) where
    writer = lift . writer
    tell = lift . tell
    listen = mapPromptT listen
    pass = mapPromptT pass

instance Applicative t => MonadPrompt a b (PromptT a b t) where
    prompt = promptP
    prompts = promptsP

-- | Directly construct a 'PromptT'.  Has to be able to take a @(a - m (t
-- b)) -> m (t r)@ that can work on /any/ 'Monad'.
--
-- Typically this won't be used, but is provided for completion; using
-- 'prompt' and its 'Applicative', 'Monad' instances, etc., is more clear.
--
-- @
-- 'prompt' r = 'mkPromptT' $ \g -> g r
-- @
mkPromptT :: (forall m. Monad m => (a -> m (t b)) -> m (t r)) -> PromptT a b t r
mkPromptT = PromptT

-- | Directly construct a 'Prompt'.  Has to be able to take a @(a -> m b)
-- -> m r@ that can work on /any/ 'Monad'.
--
-- Typically this won't be used, but is provided for completion; using
-- 'prompt' and its 'Applicative', 'Monad' instances, etc., is more clear.
mkPrompt :: (forall m. Monad m => (a -> m b) -> m r) -> Prompt a b r
#if MIN_VERSION_base(4,8,0)
mkPrompt f = PromptT $ \g -> Identity <$> f (fmap runIdentity . g)
#else
mkPrompt f = PromptT $ \g -> Identity `liftM` f (liftM runIdentity . g)
#endif

-- | Maps the underying @t a@ returned by 'PromptT'.  Cannot change @t@.
mapPromptT :: (t r -> t s) -> PromptT a b t r -> PromptT a b t s
#if MIN_VERSION_base(4,8,0)
mapPromptT f (PromptT p) = PromptT $ fmap f . p
#else
mapPromptT f (PromptT p) = PromptT $ liftM f . p
#endif

-- | Swap out the 'Traversable' @t@ with a pair of natural transformations.
-- The first maps the output @t a@, and the second maps the result of the
-- prompting function.
hoistP :: (forall s. t s -> u s)    -- ^ forward natural transformation
       -> (forall s. u s -> t s)    -- ^ backwards natural transformation
       -> PromptT a b t r
       -> PromptT a b u r
#if MIN_VERSION_base(4,8,0)
hoistP to from (PromptT p) = PromptT $ \g -> to <$> p (fmap from . g)
#else
hoistP to from (PromptT p) = PromptT $ \g -> to `liftM` p (liftM from . g)
#endif

-- | Like 'lift', but without the 'Monad' constraint.
liftP :: t r -> PromptT a b t r
liftP x = PromptT $ const (return x)

-- | Like 'prompt', but specialized to 'PromptT' and without
-- the 'Applicative' constraint.
promptP :: a                    -- ^ prompting value
        -> PromptT a b t b
promptP r = PromptT ($ r)

-- | Like 'prompts', but specialized to 'PromptT' and downgrading the
-- 'Applicative' constraint to a 'Functor' constraint.
promptsP :: Functor t
         => (b -> c)            -- ^ to be applied to response value
         -> a                   -- ^ prompting value
         -> PromptT a b t c
#if MIN_VERSION_base(4,8,0)
promptsP f r = PromptT $ (fmap . fmap) f . ($ r)
#else
promptsP f r = PromptT $ (liftM . fmap) f . ($ r)
#endif

-- | Like 'prompt'', but specialized to 'PromptT' and without the
-- 'Applicative' constraint.  Is a 'promptP' strict on its argument.
promptP' :: a                   -- ^ prompting value (strict)
         -> PromptT a b t b
promptP' x = x `seq` promptP x

-- | Like 'prompts'', but specialized to 'PromptT' and downgrading the
-- 'Applicative' constraint to a 'Functor' constraint.  Is a 'promptsP'
-- strict on its argument.
promptsP' :: Functor t
          => (b -> c)           -- ^ to be applied to response value
          -> a                  -- ^ prompting value (strict)
          -> PromptT a b t c
promptsP' f x = x `seq` promptsP f x

-- | Run a @'PromptT' a b t r@ with a given effectful @a -> m (t b)@
-- "prompt response" function, to get the resulting @r@ in @m@ and @t@.
-- The "prompt response" function is able to interact with the underlying
-- 'Traversable' @t@.
--
-- Note that the 'PromptT' in general has nothing to do with the @m@, and
-- cannot execute arbitrary @m@ other than that given in the prompt
-- response function.
runPromptTM :: Monad m
            => PromptT a b t r
            -> (a -> m (t b))   -- ^ "Prompt response function",
                                -- effectfully responding to a given @a@ with a @b@.
            -> m (t r)
runPromptTM (PromptT p) = p

-- | Run a @'Prompt' a b r@ with a given effectful @a -> m b@ "prompt
-- response" function, to get the resulting @r@ in @m@.  Note that the
-- 'Prompt' itself in general has nothing to do with @m@, and cannot
-- execute arbitrary @m@ other than that given in the prompt response
-- function.
--
-- Effectively treats a @'Prompt' a b@ as a @forall m. ReaderT (a -> m b) m@
runPromptM :: Monad m
           => Prompt a b r
           -> (a -> m b)   -- ^ "Prompt response function", effectfully
                           -- responding to a given @a@ with a @b@.
           -> m r
#if MIN_VERSION_base(4,8,0)
runPromptM (PromptT p) f = runIdentity <$> p (fmap Identity . f)
#else
runPromptM (PromptT p) f = runIdentity `liftM` p (liftM Identity . f)
#endif

-- | Run a @'PromptT' a b t r@ with a given @a -> t b@ function, with
-- 'Traversable' @t@.  The effects take place in the same context as the
-- underlying context of the 'PromptT'.
runPromptT :: PromptT a b t r
           -> (a -> t b)    -- ^ "Prompt response function", "purely"
                            -- responding to a given @a@ with a @b@ in
                            -- context of 'Traversable' @t@.
           -> t r
runPromptT (PromptT p) f = runIdentity $ p (Identity . f)

-- | Run a @'Prompt' a b r@ with a pure @a -> b@ prompt response function.
-- More or less reduces @'Prompt' a b@ to a @'Reader' (a -> b)@.
runPrompt :: Prompt a b r
          -> (a -> b)   -- ^ "Prompt response function", purely responding
                        -- to a given @a@ with a @b@.
          -> r
runPrompt (PromptT p) f = runIdentity . runIdentity $ p (Identity . Identity . f)

-- | Run a @'PromptT' String String@ in IO by sending the request to stdout
-- and reading the response from stdin.
interactPT :: Applicative t => PromptT String String t r -> IO (t r)
interactPT = flip runPromptTM $ \str -> do
    putStrLn str
    pure <$> getLine

-- | Run a @'Prompt' String String@ in IO by sending the request to stdout
-- and reading the response from stdin.
interactP :: Prompt String String r -> IO r
interactP = flip runPromptM $ \str -> do
    putStrLn str
    getLine