{-
Bustle.GDBusMessage: bindings for GDBusMessage
Copyright © 2020 Will Thompson

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
-}
{-# LANGUAGE ForeignFunctionInterface #-}
module Bustle.GDBusMessage
  (
-- * Types
    GDBusMessage
  , MessageType(..)
  , Serial

  , BusName
  , formatBusName
  , busName_

  , ObjectPath
  , formatObjectPath
  , objectPath_

  , InterfaceName
  , formatInterfaceName
  , interfaceName_

  , MemberName
  , formatMemberName
  , memberName_

-- * Constructors
  , makeNewGDBusMessage
  , wrapNewGDBusMessage
  , messageNewSignal

-- * Methods
  , messageType
  , messageSerial
  , messageReplySerial
  , messageSender
  , messageDestination
  , messageErrorName
  , messagePath
  , messageInterface
  , messageMember

  , messagePrintBody
  , messageGetBodyString
  )
where

import Data.Word
import Data.String

import Foreign.ForeignPtr
import Foreign.Ptr
import Foreign.C
import Foreign.Marshal.Alloc

import System.Glib.GObject
import System.Glib.UTFString

import Control.Monad.Trans (liftIO)
import Control.Monad.Trans.Maybe

import Bustle.GVariant

data MessageType = MessageTypeInvalid
                 | MessageTypeMethodCall
                 | MessageTypeMethodReturn
                 | MessageTypeError
                 | MessageTypeSignal
  deriving
    (Show, Ord, Eq, Enum)

-- 0 is unused in the wire protocol so indicates "no serial"
type Serial = Word32

newtype BusName = BusName String
    deriving (Eq, Ord, Show)

instance IsString BusName where
    fromString = busName_

newtype ObjectPath = ObjectPath String
    deriving (Eq, Ord, Show)

instance IsString ObjectPath where
    fromString = objectPath_

newtype InterfaceName = InterfaceName String
    deriving (Eq, Ord, Show)

newtype MemberName = MemberName String
    deriving (Eq, Ord, Show)

instance IsString MemberName where
    fromString = memberName_

-- TODO: validate
busName_ :: String
         -> BusName
busName_ = BusName

formatBusName :: BusName
              -> String
formatBusName (BusName n) = n

objectPath_ :: String
            -> ObjectPath
objectPath_ = ObjectPath

formatObjectPath :: ObjectPath
                 -> String
formatObjectPath (ObjectPath n) = n

interfaceName_ :: String
               -> InterfaceName
interfaceName_ = InterfaceName

formatInterfaceName :: InterfaceName
                    -> String
formatInterfaceName (InterfaceName n) = n

memberName_ :: String
            -> MemberName
memberName_ = MemberName

formatMemberName :: MemberName
                 -> String
formatMemberName (MemberName n) = n

newtype GDBusMessage = GDBusMessage { unGDBusMessage :: ForeignPtr GDBusMessage }
    deriving (Eq, Ord, Show)

mkGDBusMessage :: (ForeignPtr GDBusMessage -> GDBusMessage, FinalizerPtr a)
mkGDBusMessage = (GDBusMessage, objectUnref)

instance GObjectClass GDBusMessage where
    toGObject = GObject . castForeignPtr . unGDBusMessage
    unsafeCastGObject = GDBusMessage . castForeignPtr . unGObject


makeNewGDBusMessage :: IO (Ptr GDBusMessage)
                    -> IO GDBusMessage
makeNewGDBusMessage = makeNewGObject mkGDBusMessage

wrapNewGDBusMessage :: IO (Ptr GDBusMessage)
                    -> IO GDBusMessage
wrapNewGDBusMessage = wrapNewGObject mkGDBusMessage

-- Foreign imports
foreign import ccall unsafe "g_dbus_message_new_signal"
    g_dbus_message_new_signal :: CString
                              -> CString
                              -> CString
                              -> IO (Ptr GDBusMessage)

foreign import ccall unsafe "g_dbus_message_get_message_type"
    g_dbus_message_get_message_type :: Ptr GDBusMessage
                                    -> IO Int

foreign import ccall unsafe "g_dbus_message_get_serial"
    g_dbus_message_get_serial :: Ptr GDBusMessage
                              -> IO Word32

foreign import ccall unsafe "g_dbus_message_get_reply_serial"
    g_dbus_message_get_reply_serial :: Ptr GDBusMessage
                                    -> IO Word32

foreign import ccall unsafe "g_dbus_message_get_sender"
    g_dbus_message_get_sender :: Ptr GDBusMessage
                              -> IO CString

foreign import ccall unsafe "g_dbus_message_get_destination"
    g_dbus_message_get_destination :: Ptr GDBusMessage
                                   -> IO CString

foreign import ccall unsafe "g_dbus_message_get_error_name"
    g_dbus_message_get_error_name :: Ptr GDBusMessage
                                  -> IO CString

foreign import ccall unsafe "g_dbus_message_get_path"
    g_dbus_message_get_path :: Ptr GDBusMessage
                                  -> IO CString

foreign import ccall unsafe "g_dbus_message_get_interface"
    g_dbus_message_get_interface :: Ptr GDBusMessage
                                  -> IO CString

foreign import ccall unsafe "g_dbus_message_get_member"
    g_dbus_message_get_member :: Ptr GDBusMessage
                                  -> IO CString

foreign import ccall unsafe "g_dbus_message_get_body"
    g_dbus_message_get_body :: Ptr GDBusMessage
                            -> IO (Ptr GVariant)

-- Bindings

messageNewSignal :: ObjectPath
                              -> InterfaceName
                              -> MemberName
                              -> IO GDBusMessage
messageNewSignal (ObjectPath o) (InterfaceName i) (MemberName m) =
    withCString o $ \o_ptr ->
    withCString i $ \i_ptr ->
    withCString m $ \m_ptr ->
        wrapNewGDBusMessage $ g_dbus_message_new_signal o_ptr i_ptr m_ptr

messageType :: GDBusMessage
            -> IO MessageType
messageType message =
    withForeignPtr (unGDBusMessage message) $ \c_message ->
    toEnum <$> g_dbus_message_get_message_type c_message

messageSerial :: GDBusMessage
              -> IO Serial
messageSerial message =
    withForeignPtr (unGDBusMessage message) $ \c_message ->
    g_dbus_message_get_serial c_message

messageReplySerial :: GDBusMessage
                   -> IO Serial
messageReplySerial message =
    withForeignPtr (unGDBusMessage message) $ \c_message ->
    g_dbus_message_get_reply_serial c_message

messageStr :: (String -> a)
           -> (Ptr GDBusMessage -> IO CString)
           -> GDBusMessage
           -> IO (Maybe a)
messageStr ctor f message =
    withForeignPtr (unGDBusMessage message) $ \c_message -> do
    c_str <- f c_message
    if c_str == nullPtr
        then return Nothing
        else Just . ctor <$> peekUTFString c_str

messageSender :: GDBusMessage
              -> IO (Maybe BusName)
messageSender = messageStr BusName g_dbus_message_get_sender

messageDestination :: GDBusMessage
                   -> IO (Maybe BusName)
messageDestination = messageStr BusName g_dbus_message_get_destination

messageErrorName :: GDBusMessage
                 -> IO (Maybe String)
messageErrorName = messageStr id g_dbus_message_get_error_name

messagePath :: GDBusMessage
            -> IO (Maybe ObjectPath)
messagePath = messageStr ObjectPath g_dbus_message_get_path

messageInterface :: GDBusMessage
                 -> IO (Maybe InterfaceName)
messageInterface = messageStr InterfaceName g_dbus_message_get_interface

messageMember :: GDBusMessage
              -> IO (Maybe MemberName)
messageMember = messageStr MemberName g_dbus_message_get_member

messageGetBody :: GDBusMessage
               -> IO (Maybe GVariant)
messageGetBody message = do
    body <- liftIO $ withForeignPtr (unGDBusMessage message) g_dbus_message_get_body
    if body == nullPtr
        then return Nothing
        else Just <$> makeNewGVariant (return body)

messagePrintBody :: GDBusMessage
                 -> IO String
messagePrintBody message = do
    body <- messageGetBody message
    case body of
        Nothing -> return ""
        Just b  -> variantPrint b WithAnnotations

messageGetBodyString :: GDBusMessage
                     -> Word
                     -> IO (Maybe String)
messageGetBodyString message i = runMaybeT $ do
    body <- MaybeT $ messageGetBody message
    child <- MaybeT $ variantGetChild body i
    MaybeT $ variantGetString child
