{-# LANGUAGE DeriveFunctor, FlexibleContexts, FlexibleInstances, GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving, KindSignatures                #-}
{-# LANGUAGE MultiParamTypeClasses, ScopedTypeVariables                #-}
{-# LANGUAGE StandaloneDeriving, TemplateHaskell                       #-}
module Control.Monad.Refresh
       ( -- * @'RefreshT'@ type and settings
         RefreshT, runRefreshT, evalRefreshT
       , -- ** Settings for Refreshing
         RefreshSetting
       , defaultRefreshSetting
       , refresher
         -- | Environment refreshing function (default: @return@).
         --
         --   Since 0.1.0.0
       , refreshDelay
         -- | Delay in microseconds before environmental refreshment (default: @100ms@).
         --
         --   Since 0.1.0.0
       , shouldRefresh
         -- | Condition to determine if environment should be refreshed (default: @const True@).
         --
         --   Since 0.1.0.0
       , isRefreshingError
         -- | If this exception should occur an envionment refreshment? (default: refresh for any exception).
         --
         --   Since 0.1.0.0
       , -- * Transaction Combinators
         atomic
       , atomicLift
       , atomicLiftIO
       , withEnv
       , refresh
       ) where
import Control.Concurrent  (threadDelay)
import Control.Exception   (SomeException (..))
import Control.Lens        (makeLenses, view, (^.))
import Control.Monad.Catch (MonadCatch (..), catchIf)
import Control.Monad.RWS   (MonadTrans (..), RWST (..), ask, evalRWST, get,
                            gets)
import Control.Monad.RWS   (MonadIO (liftIO), MonadReader (..), modify, runRWST)
import Data.Default        (Default (..))
import Data.Typeable       (Typeable)

-- | Settings for Refreshment
--
--   Since 0.1.0.0
data RefreshSetting s m =
  RefreshSetting { _refresher         :: s -> m s
                 , _refreshDelay      :: Int
                 , _isRefreshingError :: SomeException -> Bool
                 , _shouldRefresh     :: s -> m Bool
                 }
  deriving (Typeable)

-- | Since 0.1.0.0
instance Monad m => Default (RefreshSetting s m) where
  def = RefreshSetting return (10 ^ 5) (const True) (const $ return True)

defaultRefreshSetting :: Monad m => RefreshSetting s m
defaultRefreshSetting = def

makeLenses ''RefreshSetting

data Localed a = Localed { modifier :: !(a -> a)
                         , original :: !a
                         }

runLocaled :: forall t. Localed t -> t
runLocaled (Localed f a) = f a

-- | Reader monad transformer with an automatic environment refreshment.
--
--   Since 0.1.0.0
newtype RefreshT s m a =
  RefreshT { runRefreshT_ :: RWST (RefreshSetting s m) () (Localed s) m a }
  deriving (Functor, Applicative, Monad)

-- | N.B. The @'lift'@ combinator doesn't care about exceptions;
--        this is the intended behaviour, because @'lift'@ doesn't
--        come with any atomicity meaning.
--        If you want to trigger refresh after exceptions, use @'atomicLift'@.
--
--   Since 0.1.0.0
instance MonadTrans (RefreshT s) where
  lift = RefreshT . lift

-- | N.B. The @'liftIO'@ combinator doesn't care about exceptions;
--        this is the intended behaviour, because @'liftIO'@ doesn't
--        come with any atomicity meaning.
--        If you want to trigger refresh after exceptions, use @'atomicLiftIO'@.
--
--   Since 0.1.0.0
instance (MonadIO m) => MonadIO (RefreshT s m) where
  liftIO = RefreshT . liftIO

-- | Try an atomic transaction and,
--   if exceptions specified by @'isRefreshingError'@ has been raised,
--   refreshes the environment and redo the entire transaction.
--
--   Since 0.1.0.0
atomic :: (MonadIO m, MonadCatch m) => RefreshT s m a -> RefreshT s m a
atomic (RefreshT act) = RefreshT $ view isRefreshingError >>= loop
  where
    loop chk =
      catchIf
      chk act $ const $ runRefreshT_ refresh >> loop chk

-- | @'atomicLift' = 'atomic' . 'lift'@.
--
--   Since 0.1.0.0
atomicLift :: (MonadIO m, MonadCatch m) => m a -> RefreshT s m a
atomicLift = atomic . lift

-- | @'atomicLiftIO' = 'atomic' . 'liftIO'@.
--
--   Since 0.1.0.0
atomicLiftIO :: (MonadIO m, MonadCatch m) => IO a -> RefreshT s m a
atomicLiftIO = atomic . liftIO

-- | @'atomicLift'@ composed with @'Control.Monad.Reader.ask'@.
--
--   Since 0.1.0.0
withEnv :: (MonadIO m, MonadCatch m) => (s -> m a) -> RefreshT s m a
withEnv act = atomic $ lift . act =<< ask

-- | Excecute environmental computation and returns the result with the final environment.
--
--   Since 0.1.0.0
runRefreshT :: MonadCatch m => RefreshSetting s m -> s -> RefreshT s m a -> m (a, s)
runRefreshT st s act = do
  (a, s', _) <- runRWST (runRefreshT_ act) st (Localed id s)
  return (a, original s')

-- | Excecute environmental computation and returns the result, discarding the final environment.
--
--   Since 0.1.0.0
evalRefreshT :: MonadCatch m => RefreshSetting s m -> s -> RefreshT s m a -> m a
evalRefreshT st s act = fst <$> evalRWST (runRefreshT_ act) st (Localed id s)

-- | N.B. The refreshed result took place inside @'local'@
--   will be reflected outside.
--
--   Since 0.1.0.0
instance MonadIO m => MonadReader s (RefreshT s m) where
  local f (RefreshT act) = RefreshT $ do
    old <- gets modifier
    modify $ \ls -> ls { modifier = f . old }
    a <- act
    modify (\ls -> ls {modifier = old})
    return a
  ask = RefreshT $ do
    test <- view shouldRefresh
    goRefl <- lift . test =<< gets runLocaled
    if goRefl
      then do
      st <- ask
      liftIO $ threadDelay (st ^. refreshDelay)
      s' <- lift . (st ^. refresher) =<< gets original
      modify $ \ls -> ls { original = s' }
      f <- gets modifier
      return $! f s'
      else gets runLocaled

-- | Forces environmental refreshment, regardless of @'shouldRefresh'@ condition.
--
--   Since 0.1.0.0
refresh :: MonadIO m => RefreshT s m ()
refresh = RefreshT $ do
  st <- ask
  liftIO $ threadDelay (st ^. refreshDelay)
  s' <- lift . (st ^. refresher) =<< gets original
  modify $ \ls -> ls { original = s' }