{-# Language OverloadedStrings, RecordWildCards, GADTs #-}
{-|
Module: MQTT.Encoding
Copyright: Lukas Braun 2014
License: GPL-3
Maintainer: koomi+mqtt@hackerspace-bamberg.de

Binary encoding for MQTT messages.
-}

module Network.MQTT.Encoding where

import Data.Bits (Bits(..))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.ByteString.Builder
import Data.Foldable (foldMap)
import Data.Int (Int64)
import Data.Maybe (isJust)
import Data.Monoid ((<>), mconcat, mempty)
import Data.Word (Word8)
import Data.Text.Encoding (encodeUtf8)
import System.IO (Handle)

import Network.MQTT.Types

-- | Directly write a 'Message' to the buffer of a 'Handle'.
writeTo :: Handle -> Message t -> IO ()
writeTo h msg = hPutBuilder h (putMessage msg)

-- | Generate a 'Builder' for any 'Message'.
putMessage :: Message t -> Builder
putMessage Message{..} = mconcat
    [ putMqttHeader header (msgType body)
    , encodeRemaining remaining
    , lazyByteString bodyBS
    ]
  where
    -- the header contains the length of the remaining message, so we have
    -- to build the body to determine the length in bytes
    bodyBS = toLazyByteString (putBody body)
    remaining = BSL.length bodyBS


---------------------------------
-- * Fixed Header
---------------------------------

-- | Build a 'MqttHeader' for the given message type.
putMqttHeader :: MqttHeader -> Word8 -> Builder
putMqttHeader (Header dup qos retain) msgType =
    word8 $ shiftL msgType 4 .|.
            shiftL (toBit dup) 3 .|.
            shiftL (fromQoS qos) 1 .|.
            toBit retain

-- | Encode the remaining length field.
encodeRemaining :: Int64 -> Builder
encodeRemaining n =
    let (n', digit) = n `quotRem` 128
        digit' = fromIntegral digit
    in if n' > 0
         -- set top bit to indicate more digits are following
         then word8 (digit' .|. 0x80) <> encodeRemaining n'
         else word8 digit'


---------------------------------
-- * Body
---------------------------------

-- | Build the 'MessageBody' for any message type.
putBody :: MessageBody t -> Builder
putBody (MConnect connect)          = putConnect      connect
putBody (MConnAck connAck)          = putConnAck      connAck
putBody (MPublish publish)          = putPublish      publish
putBody (MPubAck simpleMsg)         = putSimple       simpleMsg
putBody (MPubRec simpleMsg)         = putSimple       simpleMsg
putBody (MPubRel simpleMsg)         = putSimple       simpleMsg
putBody (MPubComp simpleMsg)        = putSimple       simpleMsg
putBody (MSubscribe subscribe)      = putSubscribe    subscribe
putBody (MSubAck subAck)            = putSubAck       subAck
putBody (MUnsubscribe unsubscribe)  = putUnsubscribe  unsubscribe
putBody (MUnsubAck simpleMsg)       = putSimple       simpleMsg
putBody MPingReq                    = mempty
putBody MPingResp                   = mempty
putBody MDisconnect                 = mempty


putConnect :: Connect -> Builder
putConnect Connect{..} = mconcat
    [ putMqttText "MQIsdp" -- protocol
    , word8 3 -- version
    , word8 flags
    , word16BE keepAlive
    , putMqttText clientID
    , maybe mempty putTopic (fmap wTopic will)
    , maybePut (fmap wMsg will)
    , maybePut username
    , maybePut password
    ]
  where
    maybePut = maybe mempty putMqttText
    flags = shiftL (toBit (isJust username)) 7 .|.
            shiftL (toBit (isJust password)) 6 .|.
            shiftL (maybe 0 (toBit . wRetain) will) 5 .|.
            shiftL (maybe 0 (fromQoS . wQoS) will) 3 .|.
            shiftL (toBit (isJust will)) 2 .|.
            shiftL (toBit cleanSession) 1


putConnAck :: ConnAck -> Builder
putConnAck = word8 . returnCode


putPublish :: Publish -> Builder
putPublish Publish{..} = mconcat
    [ putTopic topic
    , maybe mempty putMsgID pubMsgID
    , byteString payload
    ]


putSubscribe :: Subscribe -> Builder
putSubscribe Subscribe{..} = mconcat
    [ putMsgID subscribeMsgID
    , foldMap (\(txt, qos) -> putTopic txt <> word8 (fromQoS qos)) subTopics
    ]


putSubAck :: SubAck -> Builder
putSubAck SubAck{..} = mconcat
    [ putMsgID subAckMsgID
    , foldMap (word8 . fromQoS) granted
    ]

putUnsubscribe :: Unsubscribe -> Builder
putUnsubscribe Unsubscribe{..} = mconcat
    [ putMsgID unsubMsgID
    , foldMap putTopic unsubTopics
    ]

putSimple :: SimpleMsg -> Builder
putSimple = putMsgID . msgID


---------------------------------
-- * Utility functions
---------------------------------

-- | Build a 'MsgID'.
putMsgID :: MsgID -> Builder
putMsgID = word16BE

-- | Build a length-prefixed 'MqttText'.
putMqttText :: MqttText -> Builder
putMqttText (MqttText text) = let utf = encodeUtf8 text in
    word16BE (fromIntegral (BS.length utf)) <> byteString utf

-- | Build a 'Topic'.
putTopic :: Topic -> Builder
putTopic = putMqttText . fromTopic

-- | Encode a 'QoS'.
fromQoS :: (Num a) => QoS -> a
fromQoS NoConfirm = 0
fromQoS Confirm   = 1
fromQoS Handshake = 2

-- | Convert a 'Bool' to 0 or 1.
toBit :: (Num a) => Bool -> a
toBit False = 0
toBit True = 1

-- | Encode the type of a 'MessageBody'.
msgType :: (Num a) => MessageBody t -> a
msgType (MConnect _)     = 1
msgType (MConnAck _)     = 2
msgType (MPublish _)     = 3
msgType (MPubAck _)      = 4
msgType (MPubRec _)      = 5
msgType (MPubRel _)      = 6
msgType (MPubComp _)     = 7
msgType (MSubscribe _)   = 8
msgType (MSubAck _)      = 9
msgType (MUnsubscribe _) = 10
msgType (MUnsubAck _)    = 11
msgType MPingReq         = 12
msgType MPingResp        = 13
msgType MDisconnect      = 14