{-# LANGUAGE TemplateHaskell, TypeOperators, DeriveDataTypeable #-}

module DBus.TH
  (module Data.Int,
   module Data.Word,
   Client, BusName,
   ObjectPath, InterfaceName,
   MemberName, Variant,
   connectSession, connectSystem,
   Signature (..),
   signatureResult,
   Function (..),
   (=::), as,
   function, interface,
   function', interface'
  ) where

import Control.Monad
import Data.Int
import Data.Word
import Language.Haskell.TH
import qualified Data.Text as Text
import Data.Char
import Data.List
import Data.Generics
import DBus hiding (Type, Signature)
import DBus.Client hiding (Type, Signature)

-- | Function signature
data Signature = Return Name
               | Name :-> Signature
  deriving (Eq, Show, Data, Typeable)

infixr 6 :->

-- | Function with DBus name and Haskell name
data Function = Function {
    fnName      :: String    -- ^ Function name to use in Haskell
  , fnDBusName  :: String    -- ^ Function name to use in DBus
  , fnSignature :: Signature -- ^ Function signature
    }
  deriving (Eq, Show, Data, Typeable)

-- | Create a Function from it's name and Signature.
-- Sets fnDBusName == fnName.
(=::) :: String -> Signature -> Function
name =:: sig = Function name name sig

infixr 5 =::

-- | Set specific Haskell name for Function.
as :: Function -> String -> Function
fn `as` name = fn {fnName = name}

infixl 4 `as`

nArgs :: Signature -> Int
nArgs (Return _) = 0
nArgs (_ :-> s)  = 1 + nArgs s

firstLower :: String -> String
firstLower [] = []
firstLower (x:xs) = toLower x: xs

-- | Return type name for signature
signatureResult :: Signature -> Name
signatureResult (Return name) = name
signatureResult (_ :-> sig) = signatureResult sig

-- | Generate bindings for methods in specific DBus interface.
-- If second argument is (Just prefix), then prefix will be
-- added to the beginning of all DBus names and removed from all
-- Haskell names.
interface :: String       -- ^ Bus name
          -> String       -- ^ Object name
          -> String       -- ^ Interface name
          -> Maybe String -- ^ Prefix
          -> [Function]   -- ^ List of functions
          -> Q [Dec]
interface busName objectName ifaceName mbPrefix fns =
    interface' busName (Just objectName) ifaceName mbPrefix fns

-- | Generate bindings for methods in specific DBus interface.
-- If second argument is (Just prefix), then prefix will be
-- added to the beginning of all DBus names and removed from all
-- Haskell names.
interface' :: String      -- ^ Bus name
          -> Maybe String -- ^ Just name - use fixed object name; Nothing - object name will be 2nd argument of generated functions
          -> String       -- ^ Interface name
          -> Maybe String -- ^ Prefix
          -> [Function]   -- ^ List of functions
          -> Q [Dec]
interface' busName mbObjectName ifaceName mbPrefix fns =
    concat `fmap` mapM (function' busName mbObjectName ifaceName mbPrefix) fns

-- | Generate binding for one method in specific DBus interface.
-- If second argument is (Just prefix), then prefix will be
-- added to the beginning of all DBus names and removed from all
-- Haskell names.
function :: String       -- ^ Bus name
          -> String       -- ^ Object name
          -> String       -- ^ Interface name
          -> Maybe String -- ^ Prefix
          -> Function   -- ^ Function
          -> Q [Dec]
function busName objectName ifaceName mbPrefix fn =
    function' busName (Just objectName) ifaceName mbPrefix fn

-- | Generate binding for one method in specific DBus interface.
-- If second argument is (Just prefix), then prefix will be
-- added to the beginning of all DBus names and removed from all
-- Haskell names.
function' :: String       -- ^ Bus name
          -> Maybe String -- ^ Just name - use fixed object name; Nothing - object name will be 2nd argument of generated function
          -> String       -- ^ Interface name
          -> Maybe String -- ^ Prefix
          -> Function     -- ^ Function
          -> Q [Dec]
function' busName mbObjectName ifaceName mbPrefix (Function name dbusName sig) =
    let name'     = strip name
        dbusName' = addPrefix dbusName
    in sequence [generateSignature name' sig,
                 generateImplementation name' dbusName' sig]
  where
    addPrefix :: String -> String
    addPrefix s =
      case mbPrefix of
        Nothing     -> s
        Just prefix -> prefix ++ s

    strip :: String -> String
    strip s =
      case mbPrefix of
        Nothing     -> s
        Just prefix -> if prefix `isPrefixOf` s
                         then drop (length prefix) s
                         else s

    generateSignature :: String -> Signature -> Q Dec
    generateSignature name sig = do
        dbt <- dbusType (transformType sig)
        return $ SigD (mkName $ firstLower name) dbt

    dbusType :: Type -> Q Type
    dbusType t =
      case mbObjectName of
        Just _ -> [t| Client -> $(return t) |]
        Nothing -> [t| Client -> String -> $(return t) |]

    transformType :: Signature -> Type
    transformType (Return t) =
      if t == ''()
        then AppT (ConT ''IO) (ConT ''())
        else AppT (ConT ''IO) (AppT (ConT ''Maybe) (ConT t))
    transformType (t :-> s)  = AppT (AppT ArrowT (ConT t)) (transformType s)

    generateImplementation :: String -> String -> Signature -> Q Dec
    generateImplementation name dbusName sig = do
        let bus  = mkName "bus"
        objectName <- newName "object"
        args <- replicateM (nArgs sig) (newName "x")
        body <- generateBody dbusName objectName sig args
        let varArgs = case mbObjectName of
                        Just _ -> VarP bus : map VarP args
                        Nothing -> VarP bus : VarP objectName : map VarP args
        return $ FunD (mkName $ firstLower name) [Clause varArgs (NormalB body) []]

    generateBody :: String -> Name -> Signature -> [Name] -> Q Exp
    generateBody name objectName sig names = do
        [| do
           let baseMethod = methodCall (objectPath_ $(case mbObjectName of
                                                        Just oname -> litE (StringL oname)
                                                        Nothing -> varE objectName
                                                      ))
                                       (interfaceName_ ifaceName)
                                       (memberName_ name)
               method = baseMethod {
                          methodCallDestination = Just (busName_ busName),
                          methodCallBody = $(variant names)
                        }
                      
           res <- call_ $(varE $ mkName "bus") method
           $(if signatureResult sig /= ''() 
             then [| return $ fromVariant (methodReturnBody res !! 0) |]
             else [| return () |]
            )
          |]

    variant :: [Name] -> Q Exp
    variant names = do
      exs <- mapM variant1 names
      return $ ListE exs

    variant1 :: Name -> Q Exp
    variant1 name = [| toVariant $(varE name) |]