{-# LANGUAGE CPP #-}
-- | The server definitions for the server-client communication protocol.
module Game.LambdaHack.Server.ProtocolServer
  ( -- * The communication channels
    ChanServer(..)
  , ConnServerDict  -- exposed only to be implemented, not used
    -- * The server-client communication monad
  , MonadServerReadRequest
      ( getDict  -- exposed only to be implemented, not used
      , getsDict  -- exposed only to be implemented, not used
      , modifyDict  -- exposed only to be implemented, not used
      , putDict  -- exposed only to be implemented, not used
      , liftIO  -- exposed only to be implemented, not used
      )
    -- * Protocol
  , sendUpdateAI, sendQueryAI, sendPingAI
  , sendUpdateUI, sendQueryUI, sendPingUI
    -- * Assorted
  , killAllClients, childrenServer, updateConn
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , ConnServerFaction
#endif
  ) where

import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM (TQueue, atomically)
import qualified Control.Concurrent.STM as STM
import Control.Exception.Assert.Sugar
import Control.Monad
import qualified Data.EnumMap.Strict as EM
import Data.Key (mapWithKeyM, mapWithKeyM_)
import Data.Maybe
import Game.LambdaHack.Common.Thread
import System.IO.Unsafe (unsafePerformIO)

import Game.LambdaHack.Atomic
import Game.LambdaHack.Common.Actor
import Game.LambdaHack.Common.Faction
import Game.LambdaHack.Common.MonadStateRead
import Game.LambdaHack.Common.Request
import Game.LambdaHack.Common.Response
import Game.LambdaHack.Common.State
import Game.LambdaHack.Content.ModeKind
import Game.LambdaHack.Server.DebugServer
import Game.LambdaHack.Server.MonadServer hiding (liftIO)
import Game.LambdaHack.Server.State

-- | Connection channel between the server and a single client.
data ChanServer resp req = ChanServer
  { responseS :: !(TQueue resp)
  , requestS  :: !(TQueue req)
  }

-- | Connections to the human-controlled client of a faction and
-- to the AI client for the same faction.
type ConnServerFaction = ( Maybe (ChanServer ResponseUI RequestUI)
                         , ChanServer ResponseAI RequestAI )

-- | Connection information for all factions, indexed by faction identifier.
type ConnServerDict = EM.EnumMap FactionId ConnServerFaction

-- TODO: refactor so that the monad is split in 2 and looks analogously
-- to the Client monads. Restrict the Dict to implementation modules.
-- Then on top of that implement sendQueryAI, etc.
-- For now we call it MonadServerReadRequest
-- though it also has the functionality of MonadServerWriteResponse.

-- | The server monad with the ability to communicate with clients.
class MonadServer m => MonadServerReadRequest m where
  getDict      :: m ConnServerDict
  getsDict     :: (ConnServerDict -> a) -> m a
  modifyDict   :: (ConnServerDict -> ConnServerDict) -> m ()
  putDict      :: ConnServerDict -> m ()
  liftIO       :: IO a -> m a

writeTQueueAI :: MonadServerReadRequest m
              => ResponseAI -> TQueue ResponseAI -> m ()
writeTQueueAI cmd responseS = do
  debug <- getsServer $ sniffOut . sdebugSer
  when debug $ debugResponseAI cmd
  liftIO $ atomically $ STM.writeTQueue responseS cmd

writeTQueueUI :: MonadServerReadRequest m
              => ResponseUI -> TQueue ResponseUI -> m ()
writeTQueueUI cmd responseS = do
  debug <- getsServer $ sniffOut . sdebugSer
  when debug $ debugResponseUI cmd
  liftIO $ atomically $ STM.writeTQueue responseS cmd

readTQueueAI :: MonadServerReadRequest m
             => TQueue RequestAI -> m RequestAI
readTQueueAI requestS = liftIO $ atomically $ STM.readTQueue requestS

readTQueueUI :: MonadServerReadRequest m
             => TQueue RequestUI -> m RequestUI
readTQueueUI requestS = liftIO $ atomically $ STM.readTQueue requestS

sendUpdateAI :: MonadServerReadRequest m
             => FactionId -> ResponseAI -> m ()
sendUpdateAI fid cmd = do
  conn <- getsDict $ snd . (EM.! fid)
  writeTQueueAI cmd $ responseS conn

sendQueryAI :: MonadServerReadRequest m
            => FactionId -> ActorId -> m RequestAI
sendQueryAI fid aid = do
  conn <- getsDict $ snd . (EM.! fid)
  writeTQueueAI (RespQueryAI aid) $ responseS conn
  req <- readTQueueAI $ requestS conn
  debug <- getsServer $ sniffIn . sdebugSer
  when debug $ debugRequestAI aid req
  return $! req

sendPingAI :: (MonadAtomic m, MonadServerReadRequest m)
           => FactionId -> m ()
sendPingAI fid = do
  conn <- getsDict $ snd . (EM.! fid)
  writeTQueueAI RespPingAI $ responseS conn
  -- debugPrint $ "AI client" <+> tshow fid <+> "pinged..."
  cmdPong <- readTQueueAI $ requestS conn
  -- debugPrint $ "AI client" <+> tshow fid <+> "responded."
  case cmdPong of
    ReqAIPong -> return ()
    _ -> assert `failure` (fid, cmdPong)

sendUpdateUI :: MonadServerReadRequest m
             => FactionId -> ResponseUI -> m ()
sendUpdateUI fid cmd = do
  cs <- getsDict $ fst . (EM.! fid)
  case cs of
    Nothing -> assert `failure` "no channel for faction" `twith` fid
    Just conn ->
      writeTQueueUI cmd $ responseS conn

sendQueryUI :: (MonadAtomic m, MonadServerReadRequest m)
            => FactionId -> ActorId -> m RequestUI
sendQueryUI fid aid = do
  cs <- getsDict $ fst . (EM.! fid)
  case cs of
    Nothing -> assert `failure` "no channel for faction" `twith` fid
    Just conn -> do
      writeTQueueUI RespQueryUI $ responseS conn
      req <- readTQueueUI $ requestS conn
      debug <- getsServer $ sniffIn . sdebugSer
      when debug $ debugRequestUI aid req
      return $! req

sendPingUI :: (MonadAtomic m, MonadServerReadRequest m)
           => FactionId -> m ()
sendPingUI fid = do
  cs <- getsDict $ fst . (EM.! fid)
  case cs of
    Nothing -> assert `failure` "no channel for faction" `twith` fid
    Just conn -> do
      writeTQueueUI RespPingUI $ responseS conn
      -- debugPrint $ "UI client" <+> tshow fid <+> "pinged..."
      cmdPong <- readTQueueUI $ requestS conn
      -- debugPrint $ "UI client" <+> tshow fid <+> "responded."
      case cmdPong of
        ReqUIPong ats -> mapM_ execAtomic ats
        _ -> assert `failure` (fid, cmdPong)

killAllClients :: (MonadAtomic m, MonadServerReadRequest m) => m ()
killAllClients = do
  d <- getDict
  let sendKill fid cs = do
        -- We can't check in sfactionD, because client can be from an old game.
        when (isJust $ fst cs) $
          sendUpdateUI fid $ RespUpdAtomicUI $ UpdKillExit fid
        sendUpdateAI fid $ RespUpdAtomicAI $ UpdKillExit fid
  mapWithKeyM_ sendKill d

-- Global variable for all children threads of the server.
childrenServer :: MVar [Async ()]
{-# NOINLINE childrenServer #-}
childrenServer = unsafePerformIO (newMVar [])

-- | Update connections to the new definition of factions.
-- Connect to clients in old or newly spawned threads
-- that read and write directly to the channels.
updateConn :: (MonadAtomic m, MonadServerReadRequest m)
           => (FactionId
               -> ChanServer ResponseUI RequestUI
               -> IO ())
           -> (FactionId
               -> ChanServer ResponseAI RequestAI
               -> IO ())
           -> m ()
updateConn executorUI executorAI = do
  -- Prepare connections based on factions.
  oldD <- getDict
  let mkChanServer :: IO (ChanServer resp req)
      mkChanServer = do
        responseS <- STM.newTQueueIO
        requestS <- STM.newTQueueIO
        return $! ChanServer{..}
      addConn :: FactionId -> Faction -> IO ConnServerFaction
      addConn fid fact = case EM.lookup fid oldD of
        Just conns -> return conns  -- share old conns and threads
        Nothing | fhasUI $ gplayer fact -> do
          connS <- mkChanServer
          connAI <- mkChanServer
          return (Just connS, connAI)
        Nothing -> do
          connAI <- mkChanServer
          return (Nothing, connAI)
  factionD <- getsState sfactionD
  d <- liftIO $ mapWithKeyM addConn factionD
  let newD = d `EM.union` oldD  -- never kill old clients
  putDict newD
  -- Spawn client threads.
  let toSpawn = newD EM.\\ oldD
  let forkUI fid connS =
        forkChild childrenServer $ executorUI fid connS
      forkAI fid connS =
        forkChild childrenServer $ executorAI fid connS
      forkClient fid (connUI, connAI) = do
        -- When a connection is reused, clients are not respawned,
        -- even if UI usage changes, but it works OK thanks to UI faction
        -- clients distinguished by positive FactionId numbers.
        forkAI fid connAI  -- AI clients always needed, e.g., for auto-explore
        maybe (return ()) (forkUI fid) connUI
  liftIO $ mapWithKeyM_ forkClient toSpawn