{-|
Module      : Jupyter.Messages.Internal
Description : Metadata and message headers for the Jupyter messaging protocol, used internally.
Copyright   : (c) Andrew Gibiansky, 2016
License     : MIT
Maintainer  : andrew.gibiansky@gmail.com
Stability   : stable
Portability : POSIX

This module contains type and class definitions pertaining to handling Jupyter messages
internally. The types defined here are generally useful for the @jupyter@ library
but will not be useful for users of the library.
-}

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Jupyter.Messages.Internal (
    -- * Message Metadata
    MessageHeader(..),
    Username(..),
    MessageType(..),
    IsMessage(..),
  ) where

-- Imports from 'base'
import           Control.Applicative (Alternative(..))
import           GHC.Exts (IsString)

-- Imports from 'aeson'
import           Data.Aeson (FromJSON, ToJSON)
import           Data.Aeson.Types (Parser, Object)

-- Imports from 'bytestring'
import           Data.ByteString (ByteString)

-- Imports from 'text'
import           Data.Text (Text)

-- Imports from 'jupyter'
import           Jupyter.UUID (UUID)

-- | A message header with some metadata.
--
-- The metadata has a variety of important information that pertains to the session maintained
-- between the client and the frontend. In addition to metadata, the message headers are used to
-- establish relationships between the messages: in particular, the message parent is used to
-- determine which request a reply corresponds to.
--
-- In addition, the message type is sent as a string with the message header.
--
-- For more information about the message headers, read the section of the Jupyter messaging
-- protocol about the
-- <https://jupyter-client.readthedocs.io/en/latest/messaging.html#the-wire-protocol wire protocol>.
data MessageHeader =
       MessageHeader
         { messageIdentifiers :: [ByteString]
         -- ^ The identifiers sent with the message. These identifiers come at the front of the message, and
         -- are ZeroMQ routing prefix, which can be zero or more socket identities. These are used, for
         -- instance, to know where to send /stdin/ requests (see the messaging spec).
         , messageParent :: Maybe MessageHeader
         -- ^ The parent header, if present. The parent header is used to establish relationships between
         -- request and reply messages and outputs published in response to requests.
         , messageMetadata :: Object
         -- ^ A free-form dict of metadata.
         , messageId :: UUID                     -- ^ A unique message UUID.
         , messageSession :: UUID                -- ^ A unique session UUID.
         , messageUsername :: Username           -- ^ The user who sent this message.
         , messageType :: MessageType
           -- ^ The type of this message. This is stored as a string, and determines how to parse the content
           -- and what to do with the message once it is parsed.
         }
  deriving (Eq, Show)

-- | A username represented as 'Text', part of the 'MessageHeader'.
newtype Username = Username Text
  deriving (Eq, Ord, Show, IsString, FromJSON, ToJSON)

-- | The type of a message, internally stored as a string.
--
-- Examples include @execute_request@, @comm_open@, and @display_data@.
newtype MessageType = MessageType { messageTypeText :: Text }
  deriving (Eq, Ord, Show, FromJSON, ToJSON, IsString)

-- | Jupyter messages are represented as a variety of datatypes, depending on where in the messaging
-- protocol the message is used (for instance, 'Jupyter.Messages.ClientRequest' or
-- 'Jupyter.Messages.Comm').
--
-- Given any message, however, you need to be able to get a 'MessageType' for it, so that when
-- encoding the message onto the wire you can include the message type in the header. The
-- 'IsMessage' typeclass provides a single method, 'getMessageType', which gets the message type of
-- any Jupyter message.
class ToJSON v => IsMessage v where
  -- | Get the message type for a Jupyter message.
  getMessageType :: v -> MessageType

  -- | Get a parser for this message type.
  --
  -- If this message type does not correspond to one of the constructors for the data type @v@, then
  -- return 'Nothing'. Otherwise, return a parser that parses the message body into the given
  -- datatype.
  --
  -- This is a slightly unusual but necessary interface, because the message type and the message body
  -- come in separate bytestrings, as they are separate blobs sent on the communication sockets. Thus,
  -- we must look first at the message type, and choose the JSON parser based on the message type.
  parseMessageContent :: MessageType -> Maybe (Object -> Parser v)

-- | Provide an 'IsMessage' instance for the sum of two types which have 'IsMessage' instances.
--
-- This is particularly useful for dealing with the /shell/ and /iopub/ socket, where kernels and
-- clients can receive different types of messages on one socket (kernels can receive 'Comm' and
-- 'ClientRequest' messages on the /shell/ socket, while clients can receive 'KernelOutput' and
-- 'Comm' messages on the /iopub/ socket).
instance (IsMessage v1, IsMessage v2) => IsMessage (Either v1 v2) where
  getMessageType = either getMessageType getMessageType
  parseMessageContent msgType =
    fmap3 Left (parseMessageContent msgType) <|> fmap3 Right (parseMessageContent msgType)
    where
      -- I can't believe I get to write `fmap . fmap . fmap` and have it be meaningful. Don't think too
      -- hard, just look at the type signature, which is specialized to the functors that I care about in
      -- this case! (Maybe, (->) Object, and Parser, to be specific).
      fmap3 :: (a -> b) -> Maybe (Object -> Parser a) -> Maybe (Object -> Parser b)
      fmap3 = fmap . fmap . fmap