"""
Copyright 2004 Jim Bublitz

Terms and Conditions

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Except as contained in this notice, the name of the copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in this Software without prior written authorization from the
copyright holder.
"""

from dcop import DCOPClient
from qt import QString, QCString, QByteArray, QDataStream, IO_ReadOnly, IO_WriteOnly
from kdecore import dcop_add, dcop_next
from kdecore import KURL

numericTypes = ["char", "short", "int", "long", "uchar", "ushort", "uint", "ulong",
                "unsigned char", "unsigned short", "unsigned int", "unsigned long",
                "Q_INT32", "pid_t", "float", "double"]
stringTypes  = ["QString", "QCString"]

"""
(Most of this code is adapted from pydcop in kde-bindings, written by
Torben Weis and Julian Rockey)

The three classes below (DCOPApp, DCOPObj and DCOPMeth)
allow transparent Python calls to DCOP methods. For example:

    d = DCOPApp ("kicker", dcop)

(where "kicker" is the complete name of an application and 'dcop' is
the dcopClient instance owned by the KApplication creating the DCOPApp
instance) creates a DCOPApp instance. All of the classes in this
file "borrow" a DCOPClient instance from the calling application.

    d.objects

will return a list of the DCOP objects the application supplies.

    o = d.object ("Panel")

will return a DCOPObj corresponding to applications "Panel" DCOP object.

Similarly:

    o.methods

will return a list of the methods the object supplies and

    m = o.method ("panelSize")

will return a DCOPMeth corresponding to Panel's panelSize() method.
The m instance also holds the methods return type, list of argument types
(argtypes) and argument names (argnames).

    m.valid

is a boolean which indicates if the method encapsulated by m is a valid
method for the application/object specified.

However it isn't necessary to explicitly create the DCOPObj and DCOPMeth.

    d.Panel.panelSize.valid

for example, will also indicate if the method is valid without creating the
intermediate 'o' and 'm' instances explicitly.

    d       = DCOPApp ("kicker", dcop)
    ok, res = d.Panel.panelSize ()

is all the code necessary to perform the indicated DCOP call and return the
value the call returns. In this case, panelSize takes no arguments and
returns an int. 'ok' returns the status of the DCOP call (success = True,
failure = False).

    ok = d.Panel.addURLButton (QString ("http://www.kde.org"))

would call addURLButton with the required argument, and return nothing but the DCOP call
status(since its return type is 'void').

Note that to instantiate a DCOPObj directly, you need to have a valid DCOPApp
to pass to DCOPObj's __init__ method. Similarly, DCOPMeth requires a valid DCOPOBject.
For example:

        d = DCOPApp ("kicker", dcop)
        o = DCOPObj (d, "Panel")
        m = DCOPMeth (o, "panelSize")

or

        m = DCOPMeth (DCOPObj (DCOPApp ("kicker", dcop), "Panel"), "panelSize")

"""


class DCOPApp(object):
    """
    An object corresponding to an application with a DCOP interface

    Can return a list of the DCOP objects the application exposes,
    or create and return an instance of a specific DCOP object.
    """
    def __init__ (self, name, client):
        self.appname   = name
        self.appclient = client

    def __getattr__ (self, item ):
        if item == "__repr__":
            return object.__repr__
        if item == "__str__":
            return object.__str__
        if item == "__call__":
            return object.__call__
        if item == "objects":
            objs, ok = self.appclient.remoteObjects (self.appname)
            if ok:
                return objs
            else:
                return None


        return DCOPObj (self, item)

    def object (self, object):
        return DCOPObj (self, object)

class DCOPObj(object):
    """
    An object corresponding to a specific DCOP object owned by a
    specific application with a DCOP interface

    Can return a list of the DCOP methods the object exposes,
    or create and return an instance of a specific DCOP method.
    """

    def __init__ (self, *args):
        if isinstance (args [0], str) or isinstance (args [0], QCString) or isinstance (args [0], QString):
            self.appname   = args [0]
            self.objclient = args [1]
            self.objname   = args [2]
        else:
            self.appname   = args [0].appname
            self.objname   = args [1]
            self.objclient = args [0].appclient

        self.objmethods = self.getMethods ()

    def __repr__( self ):
        return "DCOPObj(%s,%s)" % (self.appname, self.objname)

    def __str__( self ):
        return "DCOPObj(%s,%s)" % (self.appname, self.objname)

    def __getattr__( self, item ):
        if item == "__repr__":
            return object.__repr__
        if item == "__str__":
            return object.__str__
        if item == "__call__":
            return object.__call__
        if item == "methods":
            return self.objmethods

        return DCOPMeth (self, item)

    def getMethods (self):
        flist, ok = self.objclient.remoteFunctions (self.appname, self.objname)
        if ok:
            return flist
        else:
            return None

    def getMethodNames (self):
        if not self.objmethods:
            return None

        methNames = []
        for meth in self.objmethods:
            head, tail = str(meth).split (" ", 1)
            if not tail:
                return None

            i = tail.find ("(")
            if i < 1:
                return None

            methNames.append (tail [:i].strip ())

        return methNames

    def method(self, method):
        return DCOPMeth (self, method)

class DCOPMeth(object):
    """
    An object corresponding to a specific DCOP method owned by a
    specific DCOP object.
    """
    def __init__ (self, dcopObj, name):
        self.appname  = dcopObj.appname
        self.objname  = dcopObj.objname
        self.methname = name
        self.client   = dcopObj.objclient
        self.methods  = dcopObj.objmethods
        self.valid    = self.parseMethod ()
        if not self.valid:
            self.fcnname = self.method = self.rtype = self.argtypes = self.argnames = None


    def __repr__( self ):
        return "DCOPMeth(%s,%s,%s)" % (self.appname, self.objname, self.methname)

    def __str__( self ):
        return "DCOPMeth(%s,%s,%s)" % (self.appname, self.objname, self.methname)

    def __call__ (self, *args):
        return self.dcop_call (args)

    def dcop_call (self, args):
        # method valid?
        if not self.valid:
            return False, None

        #Remove escape characters
#        if self.objname [0] == '_':
#            self.objname = self.objname [1:]
#        if self.name [0] == '_':
#            self.name = self.name [1:]

        # correct number of args?
        if self.argtypes and len (args) != len (self.argtypes):
            return False, None

        ok, replyType, replyData = self.client.call (self.appname, self.objname, self.fcnname, self.marshall (args))

        if ok:
            return ok, self.unmarshall (replyData, replyType)
        else:
            return ok, None

    def findMethod (self):
        for meth in self.methods:
            head, tail = str(meth).split (" ", 1)
            if not tail:
                return None

            i = tail.find ("(")
            if i < 1:
                return None

            if self.methname == tail [:i].strip ():
                self.method = meth
                return True

        return False

    def parseMethod (self):
        if not self.methods or not self.findMethod ():
            return False

        self.method = str (DCOPClient.normalizeFunctionSignature (self.method)).replace (">", "> ")
        self.rtype, tail = self.method.split (" ", 1)
        if not tail:
            return False
        self.rtype = self.rtype.strip ()

        i = tail.find ("(")
        if i < 1:
            return False

        self.fcnname = tail [:i].strip () + "("

        self.argtypes = []
        self.argnames = []
        args = tail [i + 1 : -1].split (",")
        if args and args != [""]:
            for arg in args:
                argInstance = arg.split ()
                self.argtypes.append (argInstance [0].strip ())
                if len (argInstance) == 2:
                    self.argnames.append (argInstance [1].strip ())
                else:
                    self.argnames.append ("")

        self.fcnname += ",".join (self.argtypes) + ")"

        return True

    def marshall (self, args):
        if not self.valid:
            return None

        data = QByteArray ()
        if self.argtypes == []:
            return data

        params = QDataStream (data, IO_WriteOnly)
        for i in range (len (args)):
            # handle numeric type (char, short, int, long, unsigned types, float, double)
            if self.argtypes [i] in numericTypes:
                dcop_add (params, args [i], self.argtypes [i])

            # handle string type when QString or QCString expected
            elif self.argtypes [i] in stringTypes and isinstance (args [i], str):
                dcop_add (params, eval ("%s('%s')" % (self.argtypes [i], args [i])))

            # handle QMap and QValueList which PyKDE represents as a Python dict or list
            elif self.argtypes [i].startswith ("QMap") or self.argtypes [i].startswith ("QValueList"):
                dcop_add (params, args [i], self.argtypes [i])

            # all other types
            elif eval ("isinstance (args [i], %s)" % self.argtypes [i]):
                dcop_add (params, args [i])
            else:
                raise

        return data


    def unmarshall (self, data, type_):
        s = QDataStream (data, IO_ReadOnly)
        return dcop_next (s, type_)
