--------------------------------------------------------------------------------
-- | Cluster fuck of unorganized data types
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}

module Web.SocketIO.Types.Base where

--------------------------------------------------------------------------------
import              Web.SocketIO.Types.Log
import              Web.SocketIO.Types.String
import              Web.SocketIO.Types.Event

--------------------------------------------------------------------------------
import              Control.Applicative
import              Control.Concurrent.MVar.Lifted
import              Control.Concurrent.Chan.Lifted
import              Control.Monad.Reader       
import              Control.Monad.Writer
import              Control.Monad.Base
import qualified    Data.Aeson                              as Aeson
import qualified    Data.HashMap.Strict                     as H
import              Data.IORef.Lifted
import              Network.HTTP.Types.Header               (ResponseHeaders)
import              System.IO                               (Handle)

--------------------------------------------------------------------------------
-- | Information of a individual client; established after `Web.SocketIO.Types.Request.Handshake`, torn down after `Web.SocketIO.Types.Request.Disconnect`.
data Session = Session
    {   sessionSessionID :: SessionID   -- ^ Identifier
    ,   sessionState :: SessionState    -- ^ State
    ,   sessionChannelHub :: ChannelHub -- ^ Output channels
    ,   sessionListener :: [Listener]   -- ^ Server-side listeners
    ,   sessionTimeoutVar :: MVar Bool  -- ^ TTL
    }

instance Show Session where
    show (Session i s _ _ _) = "Session " 
                            ++ fromByteString i 
                            ++ " [" ++ show s ++ "]"

--------------------------------------------------------------------------------
-- | Session ID
type SessionID = ByteString 

--------------------------------------------------------------------------------
-- | Session table, consists of `SessionID`, `Session` pairs.
type SessionTable = H.HashMap SessionID Session

--------------------------------------------------------------------------------
-- | Session states
data SessionState = Connecting  -- ^ after `Web.SocketIO.Types.Request.Handshake`
                  | Connected   -- ^ after `Web.SocketIO.Types.Request.Connect`.
                  deriving (Show, Eq)

--------------------------------------------------------------------------------
-- | Fine-grained session layer requests
data SessionAction   = SessionHandshake
                     | SessionConnect
                     | SessionPolling
                     | SessionEmit Event
                     | SessionDisconnectByClient
                     | SessionDisconnectByServer

--------------------------------------------------------------------------------
-- | Server-wide configurations
data Env = Env { 
    envSessionTableRef :: IORef SessionTable, 
    envHandler :: HandlerM (), 
    envConfiguration :: Configuration,
    envLogChannel :: Chan ByteString,
    envGlobalChannel :: Chan Package
}

--------------------------------------------------------------------------------
-- | Collection of unbounded FIFO channels.
data ChannelHub = ChannelHub
    {   channelHubLocal :: Chan Package
    ,   channelHubGlobal :: Chan Package
    ,   channelHubOutput :: Chan Package
    ,   channelHubLog :: Chan ByteString
    }

--------------------------------------------------------------------------------
-- | Defines behaviors of a Socket.IO server
data Configuration = Configuration
    {   transports :: [Transport]
    ,   logLevel :: Int             -- ^ there are 4 levels, from 0 to 3: Error, Warn, Info, Debug
    ,   logTo :: Handle
    ,   heartbeats :: Bool
    ,   header :: ResponseHeaders
    ,   closeTimeout :: Int
    ,   heartbeatTimeout :: Int
    ,   heartbeatInterval :: Int
    ,   pollingDuration :: Int
    } deriving Show
    
--------------------------------------------------------------------------------
-- | Port number for a standalone Socket.IO server.
type Port = Int

--------------------------------------------------------------------------------
-- | Triggered by an `Web.SocketIO.Types.Request.Event`.
type Listener = (EventName, CallbackM ())

--------------------------------------------------------------------------------
-- | Class for `SessionID` getter.
class HasSessionID m where
    getSessionID :: m SessionID

--------------------------------------------------------------------------------
-- | Environment carried by `HandlerM`
data HandlerEnv = HandlerEnv
    {   handlerEnvChannelHub :: ChannelHub
    ,   handlerEnvSessionID :: SessionID
    }

--------------------------------------------------------------------------------
-- | Environment carried by `CallbackM`
data CallbackEnv = CallbackEnv
    {   callbackEnvEventName :: EventName
    ,   callbackEnvPayload :: Payload
    ,   callbackEnvChannelHub :: ChannelHub
    ,   callbackEnvSessionID :: SessionID
    }

--------------------------------------------------------------------------------
-- | Capable of both sending and receiving events.
--
-- Use 'liftIO' if you wanna do some IO here.
newtype HandlerM a = HandlerM { runHandlerM :: (ReaderT HandlerEnv (WriterT [Listener] IO)) a }
    deriving (Monad, Functor, Applicative, MonadIO, MonadWriter [Listener], MonadReader HandlerEnv, MonadBase IO)

instance HasSessionID HandlerM where
    getSessionID = handlerEnvSessionID <$> ask

--------------------------------------------------------------------------------
-- | Capable of only sending events.
--
-- Use 'liftIO' if you wanna do some IO here.
newtype CallbackM a = CallbackM { runCallbackM :: (WriterT [Event] (ReaderT CallbackEnv IO)) a }
    deriving (Monad, Functor, Applicative, MonadIO, MonadWriter [Event], MonadReader CallbackEnv, MonadBase IO)

instance HasSessionID CallbackM where
    getSessionID = CallbackM . lift $ callbackEnvSessionID <$> ask

--------------------------------------------------------------------------------
-- | Sends events
class Publisher m where
    -- | Sends a message to the socket that starts it.
    --
    -- @
    -- `emit` \"launch\" [\"missile\", \"nuke\"] 
    -- @
    emit    :: EventName            -- ^ name of event to trigger
            -> [Aeson.Value]        -- ^ payload to carry with
            -> m ()

    -- | Sends a message to everybody except for the socket that starts it.
    --
    -- @
    -- `broadcast` \"hide\" [\"nukes coming!\"] 
    -- @
    broadcast   :: EventName        -- ^ name of event to trigger
                -> [Aeson.Value]    -- ^ payload to carry with
                -> m ()

    -- | 

instance Publisher HandlerM where
    emit eventName reply = do
        channel <- channelHubLocal . handlerEnvChannelHub <$> ask
        writeChan channel (Private, Event eventName (Payload reply))
    broadcast eventName reply = do
        channel <- channelHubGlobal . handlerEnvChannelHub <$> ask
        sessionID <- handlerEnvSessionID <$> ask
        writeChan channel (Broadcast sessionID, Event eventName (Payload reply))

instance Publisher CallbackM where
    emit eventName reply = do
        channel <- CallbackM . lift $ channelHubLocal . callbackEnvChannelHub <$> ask
        writeChan channel (Private, Event eventName (Payload reply))
    broadcast eventName reply = do
        channel <- CallbackM . lift $ channelHubGlobal . callbackEnvChannelHub <$> ask
        sessionID <- CallbackM . lift $ callbackEnvSessionID <$> ask
        writeChan channel (Broadcast sessionID, Event eventName (Payload reply))

--------------------------------------------------------------------------------
-- | Receives events.
class Subscriber m where
    -- @
    -- 'on' \"ping\" $ do
    --     'emit' \"pong\" []
    -- @
    on  :: EventName        -- ^ name of event to listen to
        -> CallbackM ()     -- ^ callback
        -> m ()             

instance Subscriber HandlerM where
    on event callback = do
        HandlerM . lift . tell $ [(event, callback)]
  
--------------------------------------------------------------------------------
-- | Now only xhr-polling is supported. <https://github.com/LearnBoost/socket.io-spec#transport-id socket.io-spec#transport-id>
data Transport = WebSocket | XHRPolling | NoTransport deriving (Eq, Show)

instance Serializable Transport where
    serialize WebSocket = "websocket" 
    serialize XHRPolling = "xhr-polling" 
    serialize NoTransport = "unknown"