#
# CORE
# Copyright (c)2012-2013 the Boeing Company.
# See the LICENSE file included in this distribution.
#
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
#
'''
sdt.py: Scripted Display Tool (SDT3D) helper
'''

from core.constants import *
from core.api import coreapi
from coreobj import PyCoreNet, PyCoreObj
from core.netns import nodes
from urlparse import urlparse
import socket

class Sdt(object):
    ''' Helper class for exporting session objects to NRL's SDT3D.
    The connect() method initializes the display, and can be invoked
    when a node position or link has changed.
    '''
    DEFAULT_SDT_URL = "tcp://127.0.0.1:50000/"
    # default altitude (in meters) for flyto view
    DEFAULT_ALT = 2500
    # TODO: read in user's nodes.conf here; below are default node types
    #       from the GUI
    DEFAULT_SPRITES = [('router', 'router.gif'), ('host', 'host.gif'),
                       ('PC', 'pc.gif'), ('mdr', 'mdr.gif'),
                       ('prouter', 'router_green.gif'), ('xen', 'xen.gif'),
                       ('hub', 'hub.gif'), ('lanswitch','lanswitch.gif'), 
                       ('wlan', 'wlan.gif'), ('rj45','rj45.gif'), 
                       ('tunnel','tunnel.gif'),
                       ]

    class Bunch:
        ''' Helper class for recording a collection of attributes.
        '''
        def __init__(self, **kwds):
            self.__dict__.update(kwds)
    
    def __init__(self, session):
        self.session = session
        self.sock = None
        self.connected = False
        self.showerror = True
        self.url = self.DEFAULT_SDT_URL
        self.verbose = self.session.getcfgitembool('verbose', False)
        # node information for remote nodes not in session._objs
        # local nodes also appear here since their obj may not exist yet
        self.remotes = {}
        session.broker.handlers += (self.handledistributed, )
        
    def is_enabled(self):
        ''' Check for 'enablesdt' session option. Return False by default if
            the option is missing.
        '''
        if not hasattr(self.session.options, 'enablesdt'):
            return False
        enabled = self.session.options.enablesdt
        if enabled in ('1', 'true', 1, True):
            return True
        return False

    def seturl(self):
        ''' Read 'sdturl' from session options, or use the default value.
            Set self.url, self.address, self.protocol
        '''
        url = None
        if hasattr(self.session.options,'sdturl'):
            if self.session.options.sdturl != "":
                url = self.session.options.sdturl
        if url is None or url == "":
            url = self.DEFAULT_SDT_URL
        self.url = urlparse(url)
        self.address = (self.url.hostname, self.url.port)
        self.protocol = self.url.scheme

    def connect(self, flags=0):
        ''' Connect to the SDT address/port if enabled.
        '''
        if not self.is_enabled():
            return False
        if self.connected:
            return True
        if self.session.getstate() == coreapi.CORE_EVENT_SHUTDOWN_STATE:
            return False

        self.seturl()
        if self.showerror:
            self.session.info("connecting to SDT at %s://%s" \
                                  % (self.protocol, self.address))
        if self.sock is None:
            try:
                if (self.protocol.lower() == 'udp'):
                    self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                    self.sock.connect(self.address)
                else:
                    # Default to tcp
                    self.sock = socket.create_connection(self.address, 5)
            except Exception, e:
                if self.showerror:
                    self.session.warn("SDT socket connect error: %s" % e)
                self.showerror = False
                return False
        if not self.initialize():
            return False
        self.connected = True
        # refresh all objects in SDT3D when connecting after session start
        if not flags & coreapi.CORE_API_ADD_FLAG:
            if not self.sendobjs():
                return False
        return True
    
    def initialize(self):
        ''' Load icon sprites, and fly to the reference point location on
            the virtual globe.
        '''
        if not self.cmd('path "%s/icons/normal"' % CORE_DATA_DIR):
            return False
        # send node type to icon mappings
        for (type, icon) in self.DEFAULT_SPRITES:
            if not self.cmd('sprite %s image %s' % (type, icon)):
                return False
        (lat, long) = self.session.location.refgeo[:2]
        return self.cmd('flyto %.6f,%.6f,%d' % (long, lat, self.DEFAULT_ALT))
    
    def disconnect(self):
        try:
            self.sock.close()
        except:
            pass
        self.sock = None
        self.connected = False
        
    def shutdown(self):
        ''' Invoked from Session.shutdown() and Session.checkshutdown().
        '''
        self.cmd('clear all')
        self.disconnect()
        self.showerror = True
        
    def cmd(self, cmdstr):
        ''' Send an SDT command over a UDP socket. socket.sendall() is used
            as opposed to socket.sendto() because an exception is raised when
            there is no socket listener.
        '''
        if self.sock is None:
            return False
        try:
            if self.verbose:
                self.session.info("sdt: %s" % cmdstr)
            self.sock.sendall("%s\n" % cmdstr)
            return True
        except Exception, e:
            if self.showerror:
                self.session.warn("SDT connection error: %s" % e)
                self.showerror = False
            self.sock = None
            self.connected = False
            return False
        
    def updatenode(self, nodenum, flags, x, y, z,
                         name=None, type=None, icon=None):
        ''' Node is updated from a Node Message or mobility script.
        '''
        if not self.connect():
            return
        if flags & coreapi.CORE_API_DEL_FLAG:
            self.cmd('delete node,%d' % nodenum)
            return
        if x is None or y is None:
            return
        (lat, long, alt) = self.session.location.getgeo(x, y, z)
        pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt)
        if flags & coreapi.CORE_API_ADD_FLAG:
            if icon is not None:
                type = name
                icon = icon.replace("$CORE_DATA_DIR", CORE_DATA_DIR)
                icon = icon.replace("$CORE_CONF_DIR", CORE_CONF_DIR)
                self.cmd('sprite %s image %s' % (type, icon))
            self.cmd('node %d type %s label on,"%s" %s' % \
                     (nodenum, type, name, pos))
        else:
            self.cmd('node %d %s' % (nodenum, pos))

    def updatenodegeo(self, nodenum, lat, long, alt):
        ''' Node is updated upon receiving an EMANE Location Event.
            TODO: received Node Message with lat/long/alt.
        '''
        if not self.connect():
            return
        pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt)
        self.cmd('node %d %s' % (nodenum, pos))
        
    def updatelink(self, node1num, node2num, flags, wireless=False):
        ''' Link is updated from a Link Message or by a wireless model.
        '''
        if node1num is None or node2num is None:
            return
        if not self.connect():
            return
        if flags & coreapi.CORE_API_DEL_FLAG:
            self.cmd('delete link,%s,%s' % (node1num, node2num))
        elif flags & coreapi.CORE_API_ADD_FLAG:
            attr = ""
            if wireless:
                attr = " line green,2"
            else:
                attr = " line red,2"
            self.cmd('link %s,%s%s' % (node1num, node2num, attr))
    
    def sendobjs(self):
        ''' Session has already started, and the SDT3D GUI later connects.
            Send all node and link objects for display. Otherwise, nodes and
            links will only be drawn when they have been updated (e.g. moved).
        '''
        nets = []
        with self.session._objslock:
            for obj in self.session.objs():
                if isinstance(obj, PyCoreNet):
                    nets.append(obj)
                if not isinstance(obj, PyCoreObj):
                    continue
                (x, y, z) = obj.getposition()
                if x is None or y is None:
                    continue
                self.updatenode(obj.objid, coreapi.CORE_API_ADD_FLAG, x, y, z,
                                obj.name, obj.type, obj.icon)
            for nodenum in sorted(self.remotes.keys()):
                r = self.remotes[nodenum]
                (x, y, z) = r.pos
                self.updatenode(nodenum, coreapi.CORE_API_ADD_FLAG, x, y, z,
                                r.name, r.type, r.icon)

            for net in nets:
                # use tolinkmsgs() to handle various types of links
                msgs = net.tolinkmsgs(flags = coreapi.CORE_API_ADD_FLAG)
                for msg in msgs:
                    msghdr = msg[:coreapi.CoreMessage.hdrsiz]
                    flags = coreapi.CoreMessage.unpackhdr(msghdr)[1]
                    m = coreapi.CoreLinkMessage(flags, msghdr,
                                               msg[coreapi.CoreMessage.hdrsiz:])
                    n1num = m.gettlv(coreapi.CORE_TLV_LINK_N1NUMBER)
                    n2num = m.gettlv(coreapi.CORE_TLV_LINK_N2NUMBER)
                    link_msg_type = m.gettlv(coreapi.CORE_TLV_LINK_TYPE)
                    if isinstance(net, nodes.WlanNode) or \
                       isinstance(net, nodes.EmaneNode):
                        if (n1num == net.objid):
                            continue
                    wl = (link_msg_type == coreapi.CORE_LINK_WIRELESS)   
                    self.updatelink(n1num, n2num, coreapi.CORE_API_ADD_FLAG, wl)
            for n1num in sorted(self.remotes.keys()):
                r = self.remotes[n1num]
                for (n2num, wl) in r.links:
                    self.updatelink(n1num, n2num, coreapi.CORE_API_ADD_FLAG, wl)
    
    def handledistributed(self, msg):
        ''' Broker handler for processing CORE API messages as they are 
            received. This is used to snoop the Node messages and update
            node positions.
        '''
        if msg.msgtype == coreapi.CORE_API_LINK_MSG:
            return self.handlelinkmsg(msg)
        elif msg.msgtype == coreapi.CORE_API_NODE_MSG:
            return self.handlenodemsg(msg)
            
    def handlenodemsg(self, msg):
        ''' Process a Node Message to add/delete or move a node on
            the SDT display. Node properties are found in session._objs or
            self.remotes for remote nodes (or those not yet instantiated).
        '''
        # for distributed sessions to work properly, the SDT option should be
        # enabled prior to starting the session
        if not self.is_enabled():
            return False
        # node.(objid, type, icon, name) are used.
        nodenum = msg.gettlv(coreapi.CORE_TLV_NODE_NUMBER)
        if not nodenum:
            return
        x = msg.gettlv(coreapi.CORE_TLV_NODE_XPOS)
        y = msg.gettlv(coreapi.CORE_TLV_NODE_YPOS)
        z = None
        name = msg.gettlv(coreapi.CORE_TLV_NODE_NAME)
        
        nodetype = msg.gettlv(coreapi.CORE_TLV_NODE_TYPE)
        model = msg.gettlv(coreapi.CORE_TLV_NODE_MODEL)
        icon = msg.gettlv(coreapi.CORE_TLV_NODE_ICON)

        net = False
        if nodetype == coreapi.CORE_NODE_DEF or \
           nodetype == coreapi.CORE_NODE_PHYS or \
           nodetype == coreapi.CORE_NODE_XEN:
            if model is None:
                model = "router"
            type = model
        elif nodetype != None:
            type = coreapi.node_class(nodetype).type
            net = True
        else:
            type = None
            
        try:
            node = self.session.obj(nodenum)
        except KeyError:
            node = None
        if node:
            self.updatenode(node.objid, msg.flags, x, y, z,
                            node.name, node.type, node.icon)
        else:
            if nodenum in self.remotes:
                remote = self.remotes[nodenum]
                if name is None:
                    name = remote.name
                if type is None:
                    type = remote.type
                if icon is None:
                    icon = remote.icon
            else:
                remote = self.Bunch(objid=nodenum, type=type, icon=icon,
                                    name=name, net=net, links=set())
                self.remotes[nodenum] = remote
            remote.pos = (x, y, z)
            self.updatenode(nodenum, msg.flags, x, y, z, name, type, icon)
        
    def handlelinkmsg(self, msg):
        ''' Process a Link Message to add/remove links on the SDT display.
            Links are recorded in the remotes[nodenum1].links set for updating
            the SDT display at a later time.
        '''
        if not self.is_enabled():
            return False
        nodenum1 = msg.gettlv(coreapi.CORE_TLV_LINK_N1NUMBER)
        nodenum2 = msg.gettlv(coreapi.CORE_TLV_LINK_N2NUMBER)
        link_msg_type = msg.gettlv(coreapi.CORE_TLV_LINK_TYPE)
        # this filters out links to WLAN and EMANE nodes which are not drawn
        if self.wlancheck(nodenum1):
            return
        wl = (link_msg_type == coreapi.CORE_LINK_WIRELESS)
        if nodenum1 in self.remotes:
            r = self.remotes[nodenum1]
            if msg.flags & coreapi.CORE_API_DEL_FLAG:
                if (nodenum2, wl) in r.links:
                    r.links.remove((nodenum2, wl))
            else:
                r.links.add((nodenum2, wl))
        self.updatelink(nodenum1, nodenum2, msg.flags, wireless=wl)

    def wlancheck(self, nodenum):
        ''' Helper returns True if a node number corresponds to a WlanNode
            or EmaneNode.
        '''
        if nodenum in self.remotes:
            type = self.remotes[nodenum].type
            if type in ("wlan", "emane"):
                return True
        else:
            try:
                n = self.session.obj(nodenum)
            except KeyError:
                return False
            if isinstance(n, (nodes.WlanNode, nodes.EmaneNode)):
                return True
        return False