{-# 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 (..), MonadThrow (..), 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

-- | Since 0.1.1.0
instance MonadThrow m => MonadThrow (RefreshT s m) where
  throwM = RefreshT . throwM
  {-# INLINE throwM #-}

-- | N.B. When exception is @'catch'@ed, no resource refreshment will be occured.
--        This allows users a flexible control on refreshment timing.
instance MonadCatch m => MonadCatch (RefreshT s m) where
  catch (RefreshT a) h = RefreshT $ catch a (runRefreshT_ . h)
  {-# INLINE catch #-}

-- | 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' }