{-# LANGUAGE CPP                        #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
-- |
-- Module:      Control.Monad.Factory
-- Copyright:   2019 Daniel YU
-- License:     MIT
-- Maintainer:  leptonyu@gmail.com
-- Stability:   experimental
-- Portability: portable
--
-- IoC Monad in Haskell.
--
-- * Motivation
--
-- Simplify to create an application in Haskell.
--
-- When we decide to create an application using Haskell.
-- We may need using configurations, loggers as basic functions.
-- If this application needs storages, caches, etc.,
-- then we have to weaving the management of connection of these facilities into the application.
-- Connections need to be created before and be destroyed after using them.
-- There is a common strategy to manage connections, that is using `Control.Monad.Cont`.
-- Then we can encapsulate the management of connections separately.
-- For example, we can write a database plugin `Factory` @m@ @cxt@ @DBConnection@,
-- which can manage the database connections in monad @m@ with context @cxt@.
-- Context @cxt@ may be requested for getting configurations or logging functions.
-- When all the components of application are encapsulated by plugins, then running an application will be simplified.
--
-- * Factory
--
-- 'Factory' has an environment @env@, which provides anything needs by the factory. @component@ is the production of
-- the factory, it will be used by other 'Factory'. Finally to build a complete 'Factory' m () (m ()), which can be 'boot'.
--
--
module Control.Monad.Factory(
  -- * Monad
    MonadFactory(..)
  , defer
  , asksEnv
  , modifyEnv
  , withEnv
  , runEnv
  -- * Monad Instance
  , Factory(..)
  -- ** Run functions
  , running
  , boot
  -- ** With
  , within
  , withFactory
  , wrap
  , liftFT
  , natTrans
  , tryBuild
  -- * Reexport Function
  -- ** Category Arrow
  , (C.>>>)
  , (C.<<<)
  -- ** Monoid Join
  , (<>)
  -- ** Other
  , MonadThrow(..)
  , MonadCatch
  , MonadMask
  , MonadIO(..)
  , lift
  ) where

import qualified Control.Category            as C
import           Control.Monad.Catch
import           Control.Monad.Cont
import           Control.Monad.Factory.Class
import           Control.Monad.State
#if __GLASGOW_HASKELL__ < 804
import           Data.Semigroup
#endif


-- | Factory defines how to generate a @component@ under the environment @env@ in monad @m@.
-- It is similar to IoC container in oop, @env@ will provide anything to be wanted to generate @component@.
--
newtype Factory m env component
  = Factory { unFactory :: StateT env (ContT () m) component }
  deriving (Functor, Applicative, Monad, MonadState env, MonadIO)

instance MonadThrow m => MonadThrow (Factory m env) where
  {-# INLINE throwM #-}
  throwM = liftFT . throwM

instance Monad m => MonadCont (Factory m env) where
  {-# INLINE callCC #-}
  callCC a = do
    env <- get
    wrap . running env $ callCC a

instance C.Category (Factory m) where
  {-# INLINE id #-}
  id  = get
  {-# INLINE (.) #-}
  a . b = b >>= (`within` a)

instance MonadMask m => MonadFactory env m (Factory m env) where
  getEnv = get
  putEnv = put
  produce o = wrap . bracket o

-- | Running the factory.
running :: env -> Factory m env c -> (c -> m ()) -> m ()
running env pma = runContT (evalStateT (unFactory pma) env)
{-# INLINE running #-}

-- | Run the application using a specified factory.
boot :: Monad m => Factory m () (m ()) -> m ()
boot factory = running () factory id

-- | Construct factory under @env@, and adapt it to fit another @env'@.
within :: env -> Factory m env component -> Factory m env' component
within env = Factory . lift . (`evalStateT` env) . unFactory
{-# INLINE within #-}

-- | Construct factory under @env@, and adapt it to fit another @env'@.
withFactory :: (env' -> env) -> Factory m env component -> Factory m env' component
withFactory f (Factory ma) = do
  env <- get
  Factory (lift $ evalStateT ma (f env))
{-# INLINE withFactory #-}

-- | Wrap raw procedure into a 'Factory'.
wrap :: ((c -> m ()) -> m ()) -> Factory m env c
wrap = Factory . lift . ContT
{-# INLINE wrap #-}

-- | Lift a monad @m@ into a 'Factory'.
liftFT :: Monad m => m a -> Factory m env a
liftFT ma = wrap (ma >>=)
{-# INLINE liftFT #-}

-- | Nature transform of one 'Factory' with monad @n@ into another with monad @m@.
natTrans :: (n () -> m ()) -> (m () -> n ()) -> Factory n env component -> Factory m env component
natTrans fnm fmn fac = do
  env <- get
  wrap $ \fm -> fnm $ running env fac (fmn . fm)
{-# INLINE natTrans #-}

{-# INLINE tryBuild #-}
tryBuild :: Bool -> Factory n env () -> Factory n env ()
tryBuild b p = if b then p else return ()