{-# LANGUAGE NamedFieldPuns , FlexibleContexts #-}

module Data.Morpheus.Execution.Server.Subscription
  ( ClientDB
  , GQLState
  , initGQLState
  , connectClient
  , disconnectClient
  , startSubscription
  , endSubscription
  , publishEvent
  )
where

import           Control.Monad.IO.Class         ( MonadIO(liftIO) )
import           Control.Concurrent             ( MVar
                                                , modifyMVar
                                                , modifyMVar_
                                                , newMVar
                                                , readMVar
                                                )
import           Data.Foldable                  ( traverse_ )
import           Data.List                      ( intersect )
import           Data.UUID.V4                   ( nextRandom )
import           Network.WebSockets             ( Connection
                                                , sendTextData
                                                )
import           Data.HashMap.Lazy              ( empty
                                                , insert
                                                , delete
                                                , adjust
                                                , toList
                                                )

-- MORPHEUS
import           Data.Morpheus.Types.Internal.Apollo
                                                ( toApolloResponse )
import           Data.Morpheus.Types.Internal.Resolving
                                                ( Event(..)
                                                , GQLChannel(..)
                                                , SubEvent
                                                )
import           Data.Morpheus.Types.Internal.WebSocket
                                                ( ClientID
                                                , GQLClient(..)
                                                , ClientDB
                                                , GQLState
                                                , SesionID
                                                )

-- | initializes empty GraphQL state
initGQLState :: IO (GQLState m e)
initGQLState = newMVar empty

connectClient :: MonadIO m => Connection -> GQLState m e -> IO (GQLClient m e)
connectClient clientConnection gqlState = do
  clientID <- nextRandom
  let client = GQLClient { clientID , clientConnection, clientSessions = empty }
  modifyMVar_ gqlState (pure . insert clientID client)
  return client

disconnectClient :: GQLClient m e -> GQLState m e -> IO (ClientDB m e)
disconnectClient GQLClient { clientID } state = modifyMVar state removeUser
 where
  removeUser db = let s' = delete clientID db in return (s', s')

updateClientByID
  :: MonadIO m =>
     ClientID
  -> (GQLClient m e -> GQLClient m e)
  -> MVar (ClientDB m e)
  -> m ()
updateClientByID key f state = liftIO $ modifyMVar_ state (return . adjust f key)

publishEvent
  :: (Eq (StreamChannel e), GQLChannel e, MonadIO m) => GQLState m e -> e -> m ()
publishEvent gqlState event = liftIO (readMVar gqlState) >>= traverse_ sendMessage
 where
  sendMessage GQLClient { clientSessions, clientConnection }
    | null clientSessions  = return ()
    | otherwise = mapM_ send (filterByChannels clientSessions)
   where
    send (sid, Event { content = subscriptionRes }) = do
      res <- subscriptionRes event
      let apolloRes = toApolloResponse sid res
      liftIO $ sendTextData clientConnection apolloRes
    ---------------------------
    filterByChannels = filter
      ( not
      . null
      . intersect (streamChannels event)
      . channels
      . snd
      ) . toList

endSubscription :: MonadIO m => ClientID -> SesionID -> GQLState m e -> m ()
endSubscription cid sid = updateClientByID cid stopSubscription
 where
  stopSubscription client = client { clientSessions = delete sid (clientSessions client) }

startSubscription :: MonadIO m => ClientID -> SubEvent m e -> SesionID -> GQLState m e -> m ()
startSubscription cid subscriptions sid = updateClientByID cid startSubscription
 where
  startSubscription client = client
    { clientSessions = insert sid subscriptions (clientSessions client) }