{-# LANGUAGE CPP #-}
-- |
-- Module:      Boots.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'.
--
-- For example:
--
-- > factory = do
-- >   log  <-  logFactory
-- >   conf <- confFactory
-- >   within (log, conf) $ do
-- >     a <- withFactory fst aFactory
-- >     b <- withFactory snd bFactory
-- >     polish AB{..}
-- >       [ xFactory
-- >       , yFactory
-- >       ] >>> bootFactory
module Boots.Factory(
  -- * Definition
    Factory
  -- ** Run functions
  , running
  , boot
  -- * Factory Construction
  -- ** With
  , withFactory
  , within
  -- ** Polish
  , polish
  -- ** Nature Transformation
  , natTrans
  -- ** Resource
  , wrap
  , bracket
  , offer
  , delay
  -- * Reexport Function
  -- ** Category Arrow
  , (C.>>>)
  , (C.<<<)
  -- ** Monoid Join
  , (<>)
  -- ** Other
  , MonadThrow(..)
  , MonadCatch
  , MonadReader(..)
  , asks
  , MonadIO(..)
  , lift
  ) where

import qualified Control.Category     as C
import           Control.Monad.Catch  hiding (bracket)
import           Control.Monad.Cont
import           Control.Monad.Reader
import           Unsafe.Coerce        (unsafeCoerce)
#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 :: ReaderT env (ContT () m) component }
  deriving (Functor, Applicative, Monad, MonadReader env, MonadIO)

instance MonadThrow m => MonadThrow (Factory m env) where
  throwM = offer . throwM

instance Monad m => MonadCont (Factory m env) where
  callCC a = do
    env <- ask
    wrap . running env $ callCC a

instance Semigroup (Factory m env env) where
  a <> b = a >>= (`within` b)

instance Monoid (Factory m env env) where
  mempty = ask
  mappend = (<>)

instance C.Category (Factory m) where
  id  = ask
  a . b = b >>= (`within` a)

-- | Running the factory.
running :: env -> Factory m env c -> (c -> m ()) -> m ()
running env pma = runContT (runReaderT (unFactory pma) env)

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

-- | Switch factory environment.
withFactory :: (env' -> env) -> Factory m env component -> Factory m env' component
withFactory = unsafeCoerce withReaderT

-- | Construct factory under @env@, and adapt it to fit another @env'@.
within :: env -> Factory m env component -> Factory m env' component
within = withFactory . const

-- | Polish @component@ by a sequence of 'Factory', and construct a unified one.
polish :: component -> [Factory m component component] -> Factory m env' component
polish env = within env . mconcat

-- | 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 <- ask
  wrap $ \fm -> fnm $ running env fac (fmn . fm)

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

-- | Construct open-close resource into a 'Factory'.
bracket :: MonadCatch m => m res -> (res -> m ()) -> Factory m env res
bracket open close = wrap $ \f -> do
  res <- open
  a   <- try $ f res
  b   <- try $ close res
  go a b
  where
    go (Left e) _ = throwM (e :: SomeException)
    go _ (Left e) = throwM (e :: SomeException)
    go _ _        = return ()

-- | Lift a monad @m@ into a 'Factory'.
offer :: Monad m => m a -> Factory m env a
offer ma = wrap (ma >>=)

-- | Put a delay action into 'Factory', it will run at close phase.
delay :: MonadCatch m => m () -> Factory m env ()
delay ma = bracket (return ()) (const ma)