{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TypeFamilies #-}

module DBus.Introspect where

import           Blaze.ByteString.Builder
import           Control.Applicative ((<$>))
import           Control.Exception (SomeException)
import qualified Data.ByteString as BS
import           Data.Conduit (($$), ($=))
import           Data.Conduit.List (consume, sourceList)
import           Data.Data (Data)
import           Data.Functor.Identity
import           Data.Map (Map)
import qualified Data.Map as Map
import           Data.Maybe
import           Data.Singletons
import           Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import           Data.Typeable (Typeable)
import           Data.XML.Pickle hiding (Result)
import           Data.XML.Types
import           Text.XML.Stream.Parse
import           Text.XML.Stream.Render
import           Text.XML.Unresolved (toEvents, fromEvents)

import           DBus.Signature
import           DBus.Types
import           DBus.Method

data IDirection = In | Out deriving (Eq, Show, Data, Typeable)

introspectableInterfaceName :: Text
introspectableInterfaceName = "org.freedesktop.DBus.Introspectable"

directionFromText :: Text -> Either Text IDirection
directionFromText "in" = Right In
directionFromText "out" = Right Out
directionFromText d = Left $ "Not a direction: " `Text.append` d

directionToText :: IDirection -> Text
directionToText In = "in"
directionToText Out = "out"


propertyAccessFromText :: Text -> Either Text PropertyAccess
propertyAccessFromText "read" = Right Read
propertyAccessFromText "write" = Right Write
propertyAccessFromText "readwrite" = Right ReadWrite
propertyAccessFromText a = Left $ "Not a property access type: " `Text.append` a

propertyAccessToText :: PropertyAccess -> Text
propertyAccessToText Read = "read"
propertyAccessToText Write = "write"
propertyAccessToText ReadWrite = "readwrite"

data IArgument = IArgument { iArgumentName :: Maybe Text
                           , iArgumentType :: DBusType
                           , iArgumentDirection :: Maybe IDirection
                           } deriving (Eq, Show, Data, Typeable)

data IMethod = IMethod { iMethodName :: Text
                       , iMethodArguments :: [IArgument]
                       , iMethodAnnotations :: [Annotation]
                       } deriving (Eq, Show, Data, Typeable)

data ISignal = ISignal { iSignalName :: Text
                       , iSignalArguments :: [IArgument]
                       , iSignalAnnotations :: [Annotation]
                     } deriving (Eq, Show, Data, Typeable)

data IProperty = IProperty { iPropertyName :: Text
                           , iPropertyType :: DBusType
                           , iPropertyAccess :: PropertyAccess
                           , iPropertyAnnotation :: [Annotation]
                           } deriving (Eq, Show, Data, Typeable)

data IInterface = IInterface { iInterfaceName :: Text
                             , iInterfaceMethods :: [IMethod]
                             , iInterfaceSignals :: [ISignal]
                             , iInterfaceProperties :: [IProperty]
                             , iInterfaceAnnotations :: [Annotation]
                             } deriving (Eq, Show, Data, Typeable)

data INode = INode { nodeName :: Maybe Text
                   , nodeInterfaces :: [IInterface]
                   , nodeSubnodes :: [INode]
                   } deriving (Eq, Show, Data, Typeable)

xpAnnotation :: PU [Node] Annotation
xpAnnotation = xpWrap (\(name, content') -> Annotation name content')
                      (\(Annotation name content') -> (name, content'))  $
                      xpElemAttrs "annotation"
                          (xp2Tuple (xpAttribute "name" xpText)
                                    (xpAttribute "value" xpText))

xpSignature :: PU Text DBusType
xpSignature = xpPartial (eitherParseSig . Text.encodeUtf8)
                        (Text.decodeUtf8 . toSignature)

xpDirection :: PU Text IDirection
xpDirection = xpPartial directionFromText directionToText

xpPropertyAccess :: PU Text PropertyAccess
xpPropertyAccess = xpPartial propertyAccessFromText propertyAccessToText

xpArgument :: PU [Node] IArgument
xpArgument = xpWrap (\(name, tp, dir) -> IArgument name tp dir)
                    (\(IArgument name tp dir) -> (name, tp, dir)) $
                 xpElemAttrs "arg"
                     (xp3Tuple (xpAttribute' "name" xpText)
                               (xpAttribute "type" xpSignature)
                               (xpAttribute' "direction" xpDirection)
                     )

xpMethod :: PU [Node] IMethod
xpMethod = xpWrap (\(name,(args, anns)) -> IMethod name args anns )
                  (\(IMethod name args anns) -> (name,(args, anns)) ) $
               xpElem "method"
                    (xpAttribute "name" xpText)
                    (xp2Tuple (xpFindMatches xpArgument)
                              (xpFindMatches xpAnnotation))

xpSignal :: PU [Node] ISignal
xpSignal = xpWrap (\(name, (args, anns)) -> ISignal name args anns )
                  (\(ISignal name args anns) -> (name, (args, anns)) ) $
               xpElem "signal"
                    (xpAttribute "name" xpText)
                    (xp2Tuple (xpFindMatches xpArgument)
                              (xpFindMatches xpAnnotation))

xpProperty :: PU [Node] IProperty
xpProperty = xpWrap (\((name, tp, access), anns)
                         -> IProperty name tp access anns)
                    (\(IProperty name tp access anns)
                         -> ((name, tp, access), anns)) $
                 xpElem "property"
                     (xp3Tuple (xpAttribute "name" xpText)
                               (xpAttribute "type" xpSignature)
                               (xpAttribute "access" xpPropertyAccess))
                     (xpFindMatches xpAnnotation)


xpInterface :: PU [Node] IInterface
xpInterface = xpWrap (\(name, (methods, signals, properties, annotations))
                        -> IInterface name methods signals properties annotations)
                     (\(IInterface name methods signals properties annotations)
                          -> (name, (methods, signals, properties, annotations))) $
                  xpElem "interface"
                      (xpAttribute "name" xpText)
                      (xp4Tuple (xpFindMatches xpMethod)
                                (xpFindMatches xpSignal)
                                (xpFindMatches xpProperty)
                                (xpFindMatches xpAnnotation) )

xpNode :: PU [Node] INode
xpNode = xpWrap (\(name, (is, ns)) -> INode name is ns)
                (\(INode name is ns) -> (name, (is, ns))) $
            xpElem "node"
                (xpAttribute' "name" xpText)
                (xp2Tuple (xpFindMatches xpInterface)
                          (xpFindMatches xpNode))

xmlToNode :: BS.ByteString -> Either Text INode
xmlToNode xml = case sourceList [xml] $= parseBytesPos def $$ fromEvents of
    Left e -> Left (Text.pack $ show (e :: SomeException))
    Right d -> case unpickle (xpRoot . xpUnliftElems $ xpNode) $ documentRoot d of
        Left e -> Left $ Text.pack (ppUnpickleError e)
        Right r -> Right r

pubID :: ExternalID
pubID = PublicID "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
                 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"

prologue :: Prologue
prologue = Prologue { prologueBefore = []
                    , prologueDoctype =
                           Just (Doctype {doctypeName = "node"
                                         , doctypeID =
                                             Just pubID
                                         })
                    , prologueAfter = []}


nodeToXml :: INode -> BS.ByteString
nodeToXml node = toByteString . mconcat . runIdentity $
                 (sourceList (toEvents doc)
                  $= renderBuilder def
                  $$ consume)
  where
    doc = Document prologue (pickle (xpRoot . xpUnliftElems $ xpNode) node) []

introspectMethods :: [Method] -> [IMethod]
introspectMethods = map introspectMethod'
  where
    introspectMethod' m = IMethod (methodName m) (toArgs m) []
    toArgs m@(Method _ _ argDs resDs) =
        let (args, res) = methodSignature m
            (ts, rs) = argDescriptions argDs resDs
        in zipWith (\n t -> IArgument (Just n) t (Just In)) ts args
           ++ zipWith ((\r t -> IArgument (Just  r) t (Just Out))) rs res


introspectSignalArgument :: DBusType -> Text -> IArgument
introspectSignalArgument tp name =
    IArgument { iArgumentName = Just name
              , iArgumentType = tp
              , iArgumentDirection = Nothing
              }

introspectSignal :: SomeSignalDescription -> ISignal
introspectSignal (SSD (s :: SignalDescription a)) =
    ISignal { iSignalName = signalDMember s
            , iSignalArguments = zipWith introspectSignalArgument
                                    (fromSing $ (sing :: Sing a))
                                    (adToList $ signalDArguments s)

            , iSignalAnnotations = [] -- signalAnnotations s
            }

propertyAccess :: Property t -> PropertyAccess
propertyAccess Property{propertySet = mbSet, propertyGet = mbGet}
    = case (mbSet, mbGet) of
    (Just{}, Just{}) -> ReadWrite
    (Nothing, Just{}) -> Read
    (Just{}, Nothing) -> Write
    (Nothing, Nothing) -> error "iPropertyAccess: Both getter and setter are Nothing"


introspectProperty :: SomeProperty -> IProperty
introspectProperty (SomeProperty p) =
    IProperty { iPropertyName = propertyName p
              , iPropertyType = propertyType p
              , iPropertyAccess = propertyAccess p
              , iPropertyAnnotation = [] -- TODO
                                 }

introspectInterface :: Text -> Interface -> IInterface
introspectInterface n i = IInterface { iInterfaceName = n
                                     , iInterfaceMethods = introspectMethods
                                                           $ interfaceMethods i
                                     , iInterfaceSignals =
                                         introspectSignal <$> interfaceSignals i
                                     , iInterfaceProperties =
                                         introspectProperty
                                         <$> interfaceProperties i
                                     , iInterfaceAnnotations = [] -- TODO
                                     }


-- | Delete prefix from map, returns (elements with prefix delete, elements that
-- didn't have this prefix)
deletePrefix :: ObjectPath -> Map ObjectPath a -> ( Map ObjectPath a
                                                  , Map ObjectPath a)
deletePrefix pre m =
    let (sub, rest) = Map.partitionWithKey (\k _ -> pre `isPathPrefix` k) m
    in (Map.mapKeys (fromJust . stripObjectPrefix pre) sub, rest)


grabPrefixes :: Map ObjectPath a -> [(ObjectPath, a, Map ObjectPath a)]
grabPrefixes m =
    let mbMin = Map.minViewWithKey m
    in case mbMin of
        Nothing -> []
        Just ((pre, obj), m')
            -> let (sub, rest) = deletePrefix pre m'
               in (pre, obj, sub) : grabPrefixes rest

propertiesInterfaceName :: Text
propertiesInterfaceName = "org.freedesktop.DBus.Properties"

propertiesInterface :: IInterface
propertiesInterface =
    IInterface {
        iInterfaceName = "org.freedesktop.DBus.Properties"
      , iInterfaceMethods =
          [IMethod{
               iMethodName = "Get"
             , iMethodArguments =
                 [ IArgument {
                       iArgumentName = Just "interface_name"
                       , iArgumentType = DBusSimpleType TypeString
                       , iArgumentDirection = Just In}
                 , IArgument {
                       iArgumentName = Just "property_name"
                     , iArgumentType = DBusSimpleType TypeString
                     , iArgumentDirection = Just In}
                 , IArgument {
                     iArgumentName = Just "value"
                     , iArgumentType = TypeVariant
                     , iArgumentDirection = Just Out }]
             , iMethodAnnotations = []}
          , IMethod{
                iMethodName = "Set"
              , iMethodArguments =
                  [ IArgument {
                         iArgumentName = Just "interface_name"
                       , iArgumentType = DBusSimpleType TypeString
                       , iArgumentDirection = Just In }
                  , IArgument {
                         iArgumentName = Just "property_name"
                       , iArgumentType = DBusSimpleType TypeString
                       , iArgumentDirection = Just In }
                  , IArgument {
                         iArgumentName = Just "value"
                        , iArgumentType = TypeVariant
                        , iArgumentDirection = Just In}]
              , iMethodAnnotations = [] }
          , IMethod{
                iMethodName = "GetAll"
              , iMethodArguments =
                  [ IArgument {
                         iArgumentName = Just "interface_name"
                       , iArgumentType = DBusSimpleType TypeString
                       , iArgumentDirection = Just In }
                  , IArgument {
                         iArgumentName = Just "props"
                       , iArgumentType = TypeDict TypeString TypeVariant
                       , iArgumentDirection = Just Out }]
              , iMethodAnnotations = [] }]
      , iInterfaceSignals =
              [ ISignal { iSignalName = "PropertiesChanged"
                        , iSignalArguments =
                            [ IArgument {
                                   iArgumentName = Just "interface_name"
                                 , iArgumentType = DBusSimpleType TypeString
                                 , iArgumentDirection = Nothing}
                            , IArgument {
                                   iArgumentName = Just "changed_properties"
                                 , iArgumentType = TypeDict TypeString TypeVariant
                                 , iArgumentDirection = Nothing}
                            , IArgument {
                                   iArgumentName = Just "invalidated_properties"
                                 , iArgumentType =
                                     TypeArray (DBusSimpleType TypeString)
                                 , iArgumentDirection = Nothing}]
                        , iSignalAnnotations = []}]
      , iInterfaceProperties = []
      , iInterfaceAnnotations = []
      }

introspectObject :: Bool
                 -> ObjectPath
                 -> Object
                 -> Map ObjectPath Object
                 -> INode
introspectObject recurse path (Object ifaces) sub
    = INode { nodeName = Just $ objectPathToText path
            , nodeInterfaces =
                let propIface = if hasInterface propertiesInterfaceName ifaces
                                then []
                                else [propertiesInterface]
                    introspectIface =
                        if hasInterface introspectableInterfaceName ifaces
                        then []
                        else [introspectInterface introspectableInterfaceName $
                                introspectableInterface path False undefined]
                in concat [introspectIface
                          , propIface
                          , uncurry introspectInterface <$> Map.toList ifaces
                          ]

            , nodeSubnodes = (if recurse
                              then (uncurry3 $ introspectObject True)
                              else \(n, _, _) ->
                                INode { nodeName = Just $ objectPathToText n
                                      , nodeInterfaces = []
                                      , nodeSubnodes = []
                                      })
                             <$> grabPrefixes sub
            }
  where
    hasInterface :: Text -> Map Text Interface -> Bool
    hasInterface iname o = isJust $ Map.lookup iname o

uncurry3 :: (t3 -> t2 -> t1 -> t) -> (t3, t2, t1) -> t
uncurry3 f (x, y, z) = f x y z

introspectObjects :: ObjectPath -> Bool -> Objects -> INode
introspectObjects path recursive (Objects os) =
    case Map.updateLookupWithKey (\_ _ -> Nothing) path os of
           (Nothing, _) ->
               let oss = Map.mapKeys (fromMaybe "" . stripObjectPrefix path) os
               in INode { nodeName = Just $ objectPathToText path
                        , nodeInterfaces = []
                        , nodeSubnodes =
                            uncurry3 (introspectObject recursive )
                              <$> grabPrefixes oss
                        }
           (Just o, os') ->
               let oss = Map.mapKeys (fromMaybe "" . stripObjectPrefix path) os'
               in introspectObject recursive path o oss


introspect :: ObjectPath -> Bool -> Objects -> Text
introspect path recursive object' = Text.decodeUtf8 . nodeToXml
                                    $ introspectObjects path recursive object'

introspectMethod :: ObjectPath -> Bool -> Objects -> Method
introspectMethod path recursive object' =
    Method (repMethod $ (return (introspect path recursive object') :: IO Text))
           "Introspect"
           Done
           ("xml_data" :> Done)


introspectableInterface :: ObjectPath -> Bool -> Objects -> Interface
introspectableInterface path recursive o =
    Interface{ interfaceMethods = [introspectMethod path recursive o]
             , interfaceSignals = []
             , interfaceAnnotations = []
             , interfaceProperties = []
             }

introspectable :: ObjectPath -> Bool -> Objects -> Object
introspectable path recursive =
    object introspectableInterfaceName . introspectableInterface path recursive

addIntrospectable :: Objects -> Objects
addIntrospectable os =
    let ro = root "/" (Object Map.empty) <> os
        intrs = for (Map.keys (Map.delete "/" (unObjects ro))) $ \path ->
            root path (introspectable path False ro)
        intr = introspectable "/" True ro
    in os <> root "/" intr <> mconcat intrs
  where
    for = flip fmap
