-- | Utilities to implement /server-loops/ with builtin state and /TEA/-like naming.
--
-- @since 0.24.0
module Control.Eff.Concurrent.Protocol.StatefulServer
  ( Server(..)
  , Stateful
  , Effectful.Init(..)
  , startLink
  , start
  , ModelState
  , modifyModel
  , getAndModifyModel
  , modifyAndGetModel
  , getModel
  , putModel
  , getAndPutModel
  , useModel
  , preuseModel
  , zoomModel
  , logModel
  , SettingsReader
  , askSettings
  , viewSettings
  , mapEffects
  , coerceEffects
  -- * Re-exports
  , Effectful.Event(..)
  )
  where

import Control.Eff
import Control.Eff.Concurrent.Misc
import Control.Eff.Concurrent.Process
import Control.Eff.Concurrent.Protocol
import qualified Control.Eff.Concurrent.Protocol.EffectfulServer as Effectful
import Control.Eff.Extend ()
import Control.Eff.Log
import Control.Eff.Reader.Strict
import Control.Eff.State.Strict
import Control.Lens
import Data.Coerce
import Data.Default
import Data.Kind
import Data.String (fromString)
import Data.Text (Text, pack)
import Data.Typeable
import GHC.Stack (HasCallStack)
import Control.Eff.Extend (raise)
import Data.Monoid (First)
-- | A type class for server loops.
--
-- This class serves as interface for other mechanisms, for example /process supervision/
--
-- The methods of this class handle 'Event's and 'Request's for 'Pdu' instances.
--
-- Instances can by /index types/ for 'Pdu' family directly, or indirectly via the 'ServerPdu' type family.
--
-- To builder servers serving multiple protocols, use the generic 'Pdu' instances, for which 'Embeds'
-- instances exist, like 2-,3-,4-, or 5-tuple.
--
-- The naming is inspired by The Elm Architecture, without the @view@ callback.
--
-- This class is based on "Control.Eff.Concurrent.Protocol.EffectfulServer" and adds a default
-- 'State' and 'Reader' effect.
--
-- @since 0.24.0
class (Typeable (Protocol a)) => Server (a :: Type) q where
  -- | The value that defines what is required to initiate a 'Server'
  -- loop.
  data StartArgument a
  -- | The index type of the 'Event's that this server processes.
  -- This is the first parameter to the 'Request' and therefore of
  -- the 'Pdu' family.
  type Protocol a :: Type
  type Protocol a = a
  -- | Type of the /model/ data, given to every invocation of 'update'
  -- via the 'ModelState' effect.
  -- The /model/ of a server loop is changed through incoming 'Event's.
  -- It is initially calculated by 'setup'.
  data family Model a :: Type

  -- | Type of read-only state.
  type Settings a :: Type
  type Settings a = ()

  -- | Return a new 'ProcessTitle' for the stateful process,
  -- while it is running.
  --
  -- @since 0.30.0
  title :: StartArgument a -> ProcessTitle

  default title :: Typeable a => StartArgument a -> ProcessTitle
  title _ = fromString $ showSTypeable @a ""

  -- | Return an initial 'Model' and 'Settings'
  setup ::
       Endpoint (Protocol a)
    -> StartArgument a
    -> Eff q (Model a, Settings a)

  default setup ::
       (Default (Model a), Default (Settings a))
    => Endpoint (Protocol a)
    -> StartArgument a
    -> Eff q (Model a, Settings a)
  setup _ _ = pure (def, def)

  -- | Update the 'Model' based on the 'Event'.
  update ::
       Endpoint (Protocol a)
    -> StartArgument a
    -> Effectful.Event (Protocol a)
    -> Eff (ModelState a ': SettingsReader a ': q) ()

-- | This type is used to build stateful 'EffectfulServer' instances.
--
-- It is a variant of 'EffectfulServer', that comes pre-installed
-- with 'State' and 'Reader' effects.
--
-- @since 0.24.0
data Stateful a deriving Typeable

instance Server a q => Effectful.Server (Stateful a) q where
  data Init (Stateful a) = Init (StartArgument a)
  type ServerPdu (Stateful a) = Protocol a
  type ServerEffects (Stateful a) q = ModelState a ': SettingsReader a ': q

  runEffects selfEndpoint (Init sa) m = do
    (st, env) <- setup selfEndpoint sa
    runReader env (evalState st m)

  onEvent selfEndpoint (Init sa) = update selfEndpoint sa

  serverTitle (Init startArg) = title @_ @q startArg

-- | Execute the server loop.
--
-- @since 0.24.0
startLink
  :: forall a r q
  . ( HasCallStack
    , Typeable a
    , FilteredLogging (Processes q)
    , Effectful.Server (Stateful a) (Processes q)
    , Server a (Processes q)
    , HasProcesses r q
    )
  => StartArgument a -> Eff r (Endpoint (Protocol a))
startLink = Effectful.startLink . Init

-- | Execute the server loop. Please use 'startLink' if you can.
--
-- @since 0.24.0
start
  :: forall a r q
  . ( HasCallStack
    , Typeable a
    , Effectful.Server (Stateful a) (Processes q)
    , Server a (Processes q)
    , FilteredLogging (Processes q)
    , HasProcesses r q
    )
  => StartArgument a -> Eff r (Endpoint (Protocol a))
start = Effectful.start . Init

-- | The 'Eff'ect type of mutable 'Model' in a 'Server' instance.
--
-- @since 0.24.0
type ModelState a = State (Model a)

-- | Modify the 'Model' of a 'Server'.
--
-- @since 0.24.0
modifyModel :: forall a e . Member (ModelState a) e => (Model a -> Model a) -> Eff e ()
modifyModel f = getModel @a >>= putModel @a . f

-- | Modify the 'Model' of a 'Server' and return the old value.
--
-- @since 0.24.0
getAndModifyModel :: forall a e . Member (ModelState a) e => (Model a -> Model a) -> Eff e (Model a)
getAndModifyModel f = getModel @a <* modify f

-- | Modify the 'Model' of a 'Server' and return the new value.
--
-- @since 0.24.0
modifyAndGetModel :: forall a e . Member (ModelState a) e => (Model a -> Model a) -> Eff e (Model a)
modifyAndGetModel f = modifyModel @a f *> getModel @a

-- | Return the 'Model' of a 'Server'.
--
-- @since 0.24.0
getModel :: forall a e . Member (ModelState a) e => Eff e (Model a)
getModel = get

-- | Return a element selected by a 'Lens' of the 'Model' of a 'Server'.
--
-- @since 0.24.0
useModel :: forall a b e . Member (ModelState a) e => Getting b (Model a) b -> Eff e b
useModel l = view l <$> getModel @a

-- | Return a element selected by a 'Lens' of the 'Model' of a 'Server'.
--
-- @since 0.30.0
preuseModel :: forall a b e . Member (ModelState a) e => Getting (First b) (Model a) b -> Eff e (Maybe b)
preuseModel l = preview l <$> getModel @a

-- | Overwrite the 'Model' of a 'Server'.
--
-- @since 0.24.0
putModel :: forall a e . Member (ModelState a) e => Model a -> Eff e ()
putModel = put

-- | Overwrite the 'Model' of a 'Server', return the old value.
--
-- @since 0.24.0
getAndPutModel :: forall a e . Member (ModelState a) e => Model a -> Eff e (Model a)
getAndPutModel m = getModel @a <* putModel @a m

-- | Run an action that modifies portions of the 'Model' of a 'Server' defined by the given 'Lens'.
--
-- @since 0.24.0
zoomModel :: forall a b c e. Member (ModelState a) e => Lens' (Model a) b -> Eff (State b ': e) c -> Eff e c
zoomModel l a = do
  m0 <- getModel @a
  (c, m1) <- runState (view l m0) a
  modifyModel @a (l .~ m1)
  return c

-- | Log the 'Model' of a 'Server' using 'logDebug'.
--
-- @since 0.30.0
logModel
  :: forall m e q. ( Show (Model m)
                   , Member Logs e
                   , HasProcesses e q
                   , Member (ModelState m) e)
  => Text -> Eff e ()
logModel x =
  getModel @m >>= logDebug . (x <>) . pack . show

-- | The 'Eff'ect type of readonly 'Settings' in a 'Server' instance.
--
-- @since 0.24.0
type SettingsReader a = Reader (Settings a)

-- | Return the read-only 'Settings' of a 'Server'
--
-- @since 0.24.0
askSettings :: forall a e . Member (SettingsReader a) e => Eff e (Settings a)
askSettings = ask

-- | Return the read-only 'Settings' of a 'Server' as viewed through a 'Lens'
--
-- @since 0.24.0
viewSettings :: forall a b e . Member (SettingsReader a) e =>  Getting b (Settings a) b -> Eff e b
viewSettings l = view l <$> askSettings @a

-- | Map 'ModelState' and 'SettingsReader' effects.
-- Use this to embed 'update' from another 'Server' instance.
--
-- @since 0.30.0
mapEffects
  :: forall inner outer a e.
     (Settings outer -> Settings inner) -- ^ A function to get the /inner/ settings out of the /outer/ settings
  -> Lens' (Model outer) (Model inner)  -- ^ A 'Lens' to get and set the /inner/ model inside the /outer/ model
  -> Eff (ModelState inner : SettingsReader inner : e) a
  -> Eff (ModelState outer : SettingsReader outer : e) a
mapEffects innerSettings innerStateLens innerEff =
  do st0 <- getModel @outer
     s0 <- askSettings @outer
     (res, st1) <-
      raise
        (raise
          (runReader
            @(Settings inner)
            (innerSettings s0)
            (runState
              @(Model inner)
              (st0 ^. innerStateLens)
              innerEff)))
     modifyModel @outer (innerStateLens .~ st1)
     return res


-- | Coerce 'Coercible' 'ModelState' and 'SettingsReader' effects.
-- Use this to embed 'update' from a /similar/ 'Server' instance.
--
-- @since 0.30.0
coerceEffects
  :: forall inner outer a e.
     ( Coercible (Model inner) (Model outer)
     , Coercible (Model outer) (Model inner)
     , Coercible (Settings outer) (Settings inner)
     )
  => Eff (ModelState inner : SettingsReader inner : e) a
  -> Eff (ModelState outer : SettingsReader outer : e) a
coerceEffects innerEff =
  do st0 <- getModel @outer
     s0 <- askSettings @outer
     (res, st1) <-
      raise
        (raise
          (runReader
            @(Settings inner)
            (coerce s0)
            (runState
              (coerce @(Model outer) st0)
              innerEff)))
     putModel @outer (coerce st1)
     return res