###########################################################################
# cgkit - Python Computer Graphics Kit
# Copyright (C) 2004 Matthias Baas (baas@ira.uka.de)
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# cgkit homepage: http://cgkit.sourceforge.net
###########################################################################
# $Id: tunnel.py,v 1.2 2005/08/08 18:05:34 mbaas Exp $

## \file tunnel.py
## Contains the Tunnel class.

from cgtypes import *
import component
from slots import *
import _core
import socket, threading, struct
import scene

# _ImmediateForwarder
class _ImmediateForwarder(NotificationForwarder):
    """Internal class for the Tunnel component.

    This forwarder immediately sends the new value to the server part
    of the tunnel.
    """
    def __init__(self, tunnel, slot, msgid, idx):
        NotificationForwarder.__init__(self, self.sendValue)
        self.tunnel = tunnel
        self.slot = slot
        self.msgid = msgid
        self.idx = idx

    def sendValue(self):
        t = self.tunnel
        msg = t.encodeValueMessage(self.msgid, self.idx, self.slot)
        t.send(msg)

# _GatherForwarder
class _GatherForwarder(NotificationForwarder):
    """Internal class for the Tunnel component.

    This forwarder marks the slot as changed and sends a combined
    message if it's the sender slot (the last slot in the tunnel).
    """
    def __init__(self, tunnel, idx, sender):
        NotificationForwarder.__init__(self, self.markSlot)
        self.tunnel = tunnel
        self.idx = idx
        self.sender = sender

    def markSlot(self):
        self.tunnel.markAsChanged(self.idx)
        if self.sender:
            self.tunnel.sendBulk()

                 

# Tunnel
class Tunnel(component.Component):
    """%Tunnel component which can connect slots on different machines.

    A tunnel is a component that has an arbitrary number of input slots
    and a corresponding output slot for each input slot.
    The value of the input slot is simply passed to the output slot.
    The speciality of the tunnel is that both ends may reside on two
    different machines.

    \image html tunnel.png

    An instance of the tunnel component either represents the input part
    (client) or the output part (server). Value changes on the client are
    then propagated to the server. When creating either side of the tunnel
    you have to define the name and type of the slots that should be created.
    The slot types of the client and the server should always match.

    Note: ArraySlots are currently not supported.

    Example:

    \code
    # On machine A
    t = Tunnel(
       slots = [("pos", "Vec3Slot()"),
                ("rot", "Mat3Slot()")],
       host  = "<name or IP of machine B>",
    )

    # Do connections between other components and t.pos_slot/t.rot_slot
    # or set the values directly on the tunnel
    \endcode

    \code
    # On machine B
    t = Tunnel(
       server = True,
       slots = [("pos", "Vec3Slot()"),
                ("rot", "Mat3Slot()")],
    )
    
    # Make connections between t.pos_slot/t.rot_slot and other components
    # or retrieve the values directly from the tunnel
    \endcode

    \par Protocol

    The client sends its messages as UDP datagrams to the server.
    One packet may contain several individual messages that are just
    concatenated. The first two bytes of each message is the message
    ID. The remaining part depends on the ID. The byte order is always
    in big endian.

    - ID 0: Init message (must be the only message in a packet)
    - ID 1: Binary int value
    - ID 2: Binary double value
    - ID 3: Binary bool value (actually short int)
    - ID 4: Binary vec3 value (3 doubles)
    - ID 5: Binary vec4 value (4 doubles)
    - ID 6: Binary mat3 value (9 doubles)
    - ID 7: Binary mat4 value (16 doubles)
    - ID 8: Binary quat value (4 doubles)

    All the value messages are composed of the slot index number (short int),
    followed by the binary value.    
    """

    def __init__(self,
                 name = "Tunnel",
                 server = False,
                 slots = None,
                 port = 64738,
                 host = "localhost",
                 gather_messages = False,
                 verbose = False,
                 auto_insert = True):
        """Constructor.

        The argument \a slot specifies the slots to create on the tunnel.
        It must be a list of 2-tuples containing two strings. The first
        string is the name of the attribute and the second contains the
        type and initializer of the slot (in Python syntax). The order and
        types of the slots on the client and server should always match.

        The arguments \a host and \a gather_messages are only meaningful
        on the client side. If \a gather_messages is True, then value changes
        on a slot are not immediately sent but only when the last slot
        receives a new value. You can use this option if you know that
        all slot values will change at the same time. In this case it's more
        efficient to change all values and then send only one message
        containing all new values instead of sending individual messages for
        every slot.

        \param name (\c str) Component name
        \param server (\c bool) True if this is the server part
        \param slots (\c list) A list of slot definitions. Each definition is a tuple (attribute name, slot class and initial arguments).
        \param port (\c int) Port number to use for the data transfer
        \param host (\c str) Host where the tunnel server is running (name or IP address). Client only.
        \param gather_messages (\c bool) True if the messages should be combined. Client only.
        \param verbose (\c bool) True if debug messages should be printed
        \param auto_insert (\c bool) True if the component should be inserted into the scene automatically
        """
        component.Component.__init__(self, name=name, auto_insert=auto_insert)

        if slots==None:
            slots = []

        self.server = server
        self.host = host
        self.port = port
        self.sock = None
        self.slots = slots
        self.gather_messages = gather_messages
        self.verbose = verbose

        if self.server:
            self.host = socket.gethostname()
            
        # A list with tuples (slot, id)
        self.slotobjs = []
        # A dictionary with slot indices as keys. If an index is present
        # in a dictionary then the corresponding slot was modified and the
        # value has to be sent to the server
        self.changed = {}

        self.forwarders = []

        # Initialize client/server...
        if server:
            self.initServer()
        else:
            self.initClient()

    def __str__(self):
        if self.server:
            return 'Tunnel server "%s" listening at %s:%d. %d slots.'%(self.name, self.host, self.port, len(self.slots))
        else:
            return 'Tunnel client "%s". %d slots. Server at %s:%d.'%(self.name, len(self.slots), self.host, self.port)

    ## protected:

    # initClient
    def initClient(self):
        """Initialization method for the client.
        """
        # Create socket object for sending UDP datagrams
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

        # Create slot attributes...
        self.createClientSlotAttribs()

        # Send init message
        s = map(lambda x: "%s:%s"%x, self.slots)
        s = ";".join(s)
        self.send(struct.pack(">H", 0)+s)

    # initServer
    def initServer(self):
        """Initialization method for the server.
        """
        # Create slot attributes...
        self.createServerSlotAttribs()
        
        # Open UDP socket
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        ipaddr = socket.gethostbyname(self.host)
        if self.verbose:
            print "Open UDP port %d on %s (%s)"%(self.port, ipaddr, self.host)
        self.sock.bind((ipaddr, self.port))

        # Split thread with the server loop
        self.serverthread = threading.Thread(name="Tunnel-Server",
                                             target=self.serverLoop)
        self.serverthread.setDaemon(True)
        self.serverthread.start()    

    # serverLoop
    def serverLoop(self):
        """Server loop.

        This method reads messages from the previously created socket
        and process them.
        The method is run in its own thread (because reading from the
        socket is a blocking operation).
        """

        if self.verbose:
            print "Tunnel server running at port",self.port
        while 1:
            # Read a packet (blocking)
            rawdata, addr = self.sock.recvfrom(5000)
            if self.verbose:
                print "---Tunnel messages--"
            # Process all messages...
            while rawdata!="":
                # Get the id of the first message...
                try:
                    id = struct.unpack(">H", rawdata[:2])[0]
                except:
                    print "Error: Invalid UDP packet (no valid message id)."
                    rawdata = ""
                    continue

                # Process the message...
                msgdata = rawdata[2:]
                if id==0:
                    if self.verbose:
                        print "Init"
                    f = msgdata.split(";")
                    localslots = map(lambda x: x[1], list(self.iterSlotDefs(self.slots)))
                    remoteslots = map(lambda x: tuple(x.split(":")), f)
                    remoteslots = map(lambda x: x[1], list(self.iterSlotDefs(remoteslots)))
                    mismatch = True
                    if len(localslots)==len(remoteslots):
                        mismatch = localslots!=remoteslots
                    if mismatch:
                        print "%s: The types of the remote and local slots don't match."%self.name
                    rawdata = ""
                else:
                    # Decode the slot index and the value to set...
                    try:
                        n,idx,v = self.decodeValueMessage(id, msgdata)
                    except struct.error, e:
                        print e
                        rawdata = ""
                        continue
                    except ValueError, e:
                        print e
                        rawdata = ""
                        continue
                    # Prepare the next message...
                    rawdata = msgdata[n:]
                    if self.verbose:
                        print "Message id: %d - Slot:%d Value:%s"%(id,idx,v)
                    # Get the slot that will receive the new value...
                    try:
                        slot,id = self.slotobjs[idx]
                    except IndexError,e:
                        print "Error: Invalid slot index (%d)"%idx
                    # Set the new value...
                    try:
                        slot.setValue(v)
                    except:
                        print "Error: Could not assign value",v,"to slot of type",slot.typeName()

        self.sock.close()
        if self.verbose:
            print "Tunnel server stopped at port",self.port

    # markAsChanged
    def markAsChanged(self, idx):
        """Mark a slot that has changed its value.
        """
        self.changed[idx] = 1

    # send
    def send(self, msg):
        """Send one or more messages to the server.

        \param msg (\c str) Message(s)
        """
        self.sock.sendto(msg, (self.host, self.port))

    # sendBuld
    def sendBulk(self):
        """Send the values of all slots that have changed since the last call.
        """
        msgs = ""
        for idx in self.changed.keys():
            slot,id = self.slotobjs[idx]
            msgs += self.encodeValueMessage(id, idx, slot)

        self.send(msgs)
        self.changed = {}

    # createClientSlotAttribs
    def createClientSlotAttribs(self):
        slots = self.slots
        if slots==None:
            return

        self.forwarders = []
        slotnames = ["<none>", "IntSlot", "DoubleSlot", "BoolSlot", "Vec3Slot",
                     "Vec4Slot", "Mat3Slot", "Mat4Slot", "QuatSlot"]

        for varname, slotname, slotparams in self.iterSlotDefs(slots):
            # Create the slot object
            exec "slot = %s%s"%(slotname, slotparams)
            # Add the slot as attribute
            setattr(self, "%s_slot"%varname, slot)
            exec "self.addSlot('%s', self.%s_slot)"%(varname, varname)

            id = slotnames.index(slotname)
            idx = len(self.forwarders)
            
            self.slotobjs.append((slot,id))

            # Create the forwarder object
            if self.gather_messages:
                f = _GatherForwarder(self, idx, idx==len(slots)-1)
            else:
                f = _ImmediateForwarder(self, slot, id, idx)
            slot.addDependent(f)
            self.forwarders.append(f)

    # createServerSlotAttribs
    def createServerSlotAttribs(self):
        slots = self.slots
        if slots==None:
            return

        slotnames = ["<none>", "IntSlot", "DoubleSlot", "BoolSlot", "Vec3Slot",
                     "Vec4Slot", "Mat3Slot", "Mat4Slot", "QuatSlot"]

        for varname, slotname, slotparams in self.iterSlotDefs(slots):
            # Create the slot object
            exec "slot = %s%s"%(slotname, slotparams)
            # Add the slot as attribute
            setattr(self, "%s_slot"%varname, slot)
            exec "self.addSlot('%s', self.%s_slot)"%(varname, varname)

            id = slotnames.index(slotname)
            idx = len(self.forwarders)
            
            self.slotobjs.append((slot,id))


    # deleteSlotAttribs
    def deleteSlotAttribs(self):
        if self.slots==None:
            return
        
        for name,slot in self.slots:
            slotname = "%s_slot"%name
            exec "del self.%s"%slotname

        self.slots = None

    # encodeValueMessage
    def encodeValueMessage(self, id, idx, slot):
        """Encode a binary value message.

        \param id (\c int) Message ID
        \param idx (\c int) Slot index
        \param slot (\c Slot) Corresponding slot
        """

        # int
        if id==1:
            return struct.pack(">HHi", id, idx, slot.getValue())
        # double
        elif id==2:
            return struct.pack(">HHd", id, idx, slot.getValue())
        # bool
        elif id==3:
            return struct.pack(">HHh", id, idx, slot.getValue())
        # vec3
        elif id==4:
            x,y,z = slot.getValue()
            return struct.pack(">HHddd", id, idx, x, y, z)
        # vec4
        elif id==5:
            x,y,z,w = slot.getValue()
            return struct.pack(">HHdddd", id, idx, x, y, z,w)
        # mat3
        elif id==6:
            M = slot.getValue()
            return struct.pack(">HHddddddddd", id, idx, *M.toList(rowmajor=True))
        # mat4
        elif id==7:
            M = slot.getValue()
            return struct.pack(">HHdddddddddddddddd", id, idx, *M.toList(rowmajor=True))
        # quat
        elif id==8:
            q = slot.getValue()
            return struct.pack(">HHdddd", id, idx, q.w, q.x, q.y, q.z)

    # decodeValueMessage
    def decodeValueMessage(self, id, msg):
        """Decode a binary value message.

        Raises a ValueError exception if the id is invalid. If the
        size of msg is too short, a struct.error exception is thrown.

        \param id (\c int) Message ID
        \param msg (\c str) Data part of the message (may contain additional messages)
        \return Tuple (Message size, slot index, value)
        """

        # int
        if id==1:
            id, v = struct.unpack(">Hi", msg[:6])
            return 6,id,v
        # double
        elif id==2:
            id, v = struct.unpack(">Hd", msg[0:10])
            return 10,id,v
        # bool
        elif id==3:
            id, v = struct.unpack(">Hh", msg[:4])
            return 4,id,v
        # vec3
        elif id==4:
            idx, x,y,z = struct.unpack(">Hddd", msg[:26])
            return (26, idx, vec3(x,y,z))
        # vec4
        elif id==5:
            idx, x,y,z,w = struct.unpack(">Hdddd", msg[:34])
            return (34, idx, vec4(x,y,z,w))
        # mat3
        elif id==6:
            idx, m11,m12,m13,m21,m22,m23,m31,m32,m33 = struct.unpack(">Hddddddddd", msg[:74])
            return (74, idx, mat3(m11,m12,m13,m21,m22,m23,m31,m32,m33))
        # mat4
        elif id==7:
            idx, m11,m12,m13,m14,m21,m22,m23,m24,m31,m32,m33,m34,m41,m42,m43,m44 = struct.unpack(">Hdddddddddddddddd", msg[:130])
            return (130, idx, mat4(m11,m12,m13,m14,m21,m22,m23,m24,m31,m32,m33,m34,m41,m42,m43,m44))
        # quat
        elif id==8:
            idx, w,x,y,z = struct.unpack(">Hdddd", msg[:34])
            return (34, idx, quat(w,x,y,z))
        else:
            raise ValueError, "Unknown message id (%d)"%id
            

    # iterSlotDefs
    def iterSlotDefs(self, slots):
        """Generates the variable name, slot name and slot parameters.

        This generator method takes a slot definition list and generates
        3-tuples (variable name, slot name, slot parameters).

        For example, the slot definition list [("xpos", "IntSlot(2)"),
        ("ypos", "IntSlot(12)")] will generate two tuples ("xpos", "IntSlot",
        "(2)") and ("ypos", "IntSlot", "(12)").
        """
        
        for varname, slotdef in slots:
            n = slotdef.find("(")
            if n!=-1:
                slotname = slotdef[:n]
                slotparams = slotdef[n:]
            else:
                slotname = slotdef
                slotparams = "()"
            yield varname, slotname, slotparams
        
