from xml.dom.minidom import parse

from core import logger
from core.enumerations import NodeTypes
from core.misc import nodeutils
from core.service import ServiceManager
from core.xml import xmlutils


class CoreDocumentParser0(object):
    def __init__(self, session, filename, options):
        self.session = session
        self.filename = filename
        if 'dom' in options:
            # this prevents parsing twice when detecting file versions
            self.dom = options['dom']
        else:
            self.dom = parse(filename)
        self.start = options['start']
        self.nodecls = options['nodecls']

        self.np = xmlutils.get_one_element(self.dom, "NetworkPlan")
        if self.np is None:
            raise ValueError, "missing NetworkPlan!"
        self.mp = xmlutils.get_one_element(self.dom, "MotionPlan")
        self.sp = xmlutils.get_one_element(self.dom, "ServicePlan")
        self.meta = xmlutils.get_one_element(self.dom, "CoreMetaData")

        self.coords = self.getmotiondict(self.mp)
        # link parameters parsed in parsenets(), applied in parsenodes()
        self.linkparams = {}

        self.parsedefaultservices()
        self.parseorigin()
        self.parsenets()
        self.parsenodes()
        self.parseservices()
        self.parsemeta()

    def getmotiondict(self, mp):
        """
        Parse a MotionPlan into a dict with node names for keys and coordinates
        for values.
        """
        if mp is None:
            return {}
        coords = {}
        for node in mp.getElementsByTagName("Node"):
            nodename = str(node.getAttribute("name"))
            if nodename == '':
                continue
            for m in node.getElementsByTagName("motion"):
                if m.getAttribute("type") != "stationary":
                    continue
                point = m.getElementsByTagName("point")
                if len(point) == 0:
                    continue
                txt = point[0].firstChild
                if txt is None:
                    continue
                xyz = map(int, txt.nodeValue.split(','))
                z = None
                x, y = xyz[0:2]
                if len(xyz) == 3:
                    z = xyz[2]
                coords[nodename] = (x, y, z)
        return coords

    @staticmethod
    def getcommonattributes(obj):
        """
        Helper to return tuple of attributes common to nodes and nets.
        """
        node_id = int(obj.getAttribute("id"))
        name = str(obj.getAttribute("name"))
        node_type = str(obj.getAttribute("type"))
        return node_id, name, node_type

    def parsenets(self):
        linkednets = []
        for net in self.np.getElementsByTagName("NetworkDefinition"):
            node_id, name, node_type = self.getcommonattributes(net)
            nodecls = xmlutils.xml_type_to_node_class(node_type)
            if not nodecls:
                logger.warn("skipping unknown network node '%s' type '%s'", name, node_type)
                continue
            n = self.session.add_object(cls=nodecls, objid=node_id, name=name, start=self.start)
            if name in self.coords:
                x, y, z = self.coords[name]
                n.setposition(x, y, z)
            xmlutils.get_params_set_attrs(net, ("icon", "canvas", "opaque"), n)
            if hasattr(n, "canvas") and n.canvas is not None:
                n.canvas = int(n.canvas)
            # links between two nets (e.g. switch-switch)
            for ifc in net.getElementsByTagName("interface"):
                netid = str(ifc.getAttribute("net"))
                ifcname = str(ifc.getAttribute("name"))
                linkednets.append((n, netid, ifcname))
            self.parsemodels(net, n)
        # link networks together now that they all have been parsed
        for n, netid, ifcname in linkednets:
            try:
                n2 = n.session.get_object_by_name(netid)
            except KeyError:
                logger.warn("skipping net %s interface: unknown net %s", n.name, netid)
                continue
            upstream = False
            netif = n.getlinknetif(n2)
            if netif is None:
                netif = n2.linknet(n)
            else:
                netif.swapparams('_params_up')
                upstream = True
            key = (n2.name, ifcname)
            if key in self.linkparams:
                for k, v in self.linkparams[key]:
                    netif.setparam(k, v)
            if upstream:
                netif.swapparams('_params_up')

    def parsenodes(self):
        for node in self.np.getElementsByTagName("Node"):
            id, name, type = self.getcommonattributes(node)
            if type == "rj45":
                nodecls = nodeutils.get_node_class(NodeTypes.RJ45)
            else:
                nodecls = self.nodecls
            n = self.session.add_object(cls=nodecls, objid=id, name=name, start=self.start)
            if name in self.coords:
                x, y, z = self.coords[name]
                n.setposition(x, y, z)
            n.type = type
            xmlutils.get_params_set_attrs(node, ("icon", "canvas", "opaque"), n)
            if hasattr(n, "canvas") and n.canvas is not None:
                n.canvas = int(n.canvas)
            for ifc in node.getElementsByTagName("interface"):
                self.parseinterface(n, ifc)

    def parseinterface(self, n, ifc):
        """
        Parse a interface block such as:
        <interface name="eth0" net="37278">
            <address type="mac">00:00:00:aa:00:01</address>
            <address>10.0.0.2/24</address>
            <address>2001::2/64</address>
        </interface>
        """
        name = str(ifc.getAttribute("name"))
        netid = str(ifc.getAttribute("net"))
        hwaddr = None
        addrlist = []
        try:
            net = n.session.get_object_by_name(netid)
        except KeyError:
            logger.warn("skipping node %s interface %s: unknown net %s", n.name, name, netid)
            return
        for addr in ifc.getElementsByTagName("address"):
            addrstr = xmlutils.get_text_child(addr)
            if addrstr is None:
                continue
            if addr.getAttribute("type") == "mac":
                hwaddr = addrstr
            else:
                addrlist.append(addrstr)
        i = n.newnetif(net, addrlist=addrlist, hwaddr=hwaddr, ifindex=None, ifname=name)
        for model in ifc.getElementsByTagName("model"):
            self.parsemodel(model, n, n.objid)
        key = (n.name, name)
        if key in self.linkparams:
            netif = n.netif(i)
            for k, v in self.linkparams[key]:
                netif.setparam(k, v)

    def parsemodels(self, dom, obj):
        """
        Mobility/wireless model config is stored in a ConfigurableManager's
        config dict.
        """
        nodenum = int(dom.getAttribute("id"))
        for model in dom.getElementsByTagName("model"):
            self.parsemodel(model, obj, nodenum)

    def parsemodel(self, model, obj, nodenum):
        """
        Mobility/wireless model config is stored in a ConfigurableManager's
        config dict.
        """
        name = model.getAttribute("name")
        if name == '':
            return
        type = model.getAttribute("type")
        # convert child text nodes into key=value pairs
        kvs = xmlutils.get_text_elements_to_list(model)

        mgr = self.session.mobility
        # TODO: the session.confobj() mechanism could be more generic;
        #       it only allows registering Conf Message callbacks, but here
        #       we want access to the ConfigurableManager, not the callback
        if name[:5] == "emane":
            mgr = self.session.emane
        elif name[:5] == "netem":
            mgr = None
            self.parsenetem(model, obj, kvs)

        # TODO: assign other config managers here
        if mgr:
            mgr.setconfig_keyvalues(nodenum, name, kvs)

    def parsenetem(self, model, obj, kvs):
        """
        Determine interface and invoke setparam() using the parsed
        (key, value) pairs.
        """
        ifname = model.getAttribute("netif")
        peer = model.getAttribute("peer")
        key = (peer, ifname)
        # nodes and interfaces do not exist yet, at this point of the parsing,
        # save (key, value) pairs for later
        try:
            # kvs = map(lambda(k, v): (int(v)), kvs)
            kvs = map(self.numericvalue, kvs)
        except ValueError:
            logger.warn("error parsing link parameters for '%s' on '%s'", ifname, peer)
        self.linkparams[key] = kvs

    @staticmethod
    def numericvalue(keyvalue):
        (key, value) = keyvalue
        if '.' in str(value):
            value = float(value)
        else:
            value = int(value)
        return key, value

    def parseorigin(self):
        """
        Parse any origin tag from the Mobility Plan and set the CoreLocation
        reference point appropriately.
        """
        origin = xmlutils.get_one_element(self.mp, "origin")
        if not origin:
            return
        location = self.session.location
        geo = []
        attrs = ("lat", "lon", "alt")
        for i in xrange(3):
            a = origin.getAttribute(attrs[i])
            if a is not None:
                a = float(a)
            geo.append(a)
        location.setrefgeo(geo[0], geo[1], geo[2])
        scale = origin.getAttribute("scale100")
        if scale is not None:
            location.refscale = float(scale)
        point = xmlutils.get_one_element(origin, "point")
        if point is not None and point.firstChild is not None:
            xyz = point.firstChild.nodeValue.split(',')
            if len(xyz) == 2:
                xyz.append('0.0')
            if len(xyz) == 3:
                xyz = map(lambda (x): float(x), xyz)
                location.refxyz = (xyz[0], xyz[1], xyz[2])

    def parsedefaultservices(self):
        """
        Prior to parsing nodes, use session.services manager to store
        default services for node types
        """
        for node in self.sp.getElementsByTagName("Node"):
            type = node.getAttribute("type")
            if type == '':
                continue  # node-specific service config
            services = []
            for service in node.getElementsByTagName("Service"):
                services.append(str(service.getAttribute("name")))
            self.session.services.defaultservices[type] = services
            logger.info("default services for type %s set to %s" % (type, services))

    def parseservices(self):
        """
        After node objects exist, parse service customizations and add them
        to the nodes.
        """
        svclists = {}
        # parse services and store configs into session.services.configs
        for node in self.sp.getElementsByTagName("Node"):
            name = node.getAttribute("name")
            if name == '':
                continue  # node type without name
            n = self.session.get_object_by_name(name)
            if n is None:
                logger.warn("skipping service config for unknown node '%s'" % name)
                continue
            for service in node.getElementsByTagName("Service"):
                svcname = service.getAttribute("name")
                if self.parseservice(service, n):
                    if n.objid in svclists:
                        svclists[n.objid] += "|" + svcname
                    else:
                        svclists[n.objid] = svcname
        # nodes in NetworkPlan but not in ServicePlan use the
        # default services for their type
        for node in self.np.getElementsByTagName("Node"):
            id, name, type = self.getcommonattributes(node)
            if id in svclists:
                continue  # custom config exists
            else:
                svclists[int(id)] = None  # use defaults

        # associate nodes with services
        for objid in sorted(svclists.keys()):
            n = self.session.get_object(objid)
            self.session.services.addservicestonode(node=n, nodetype=n.type, services_str=svclists[objid])

    def parseservice(self, service, n):
        """
        Use session.services manager to store service customizations before
        they are added to a node.
        """
        name = service.getAttribute("name")
        svc = ServiceManager.get(name)
        if svc is None:
            return False
        values = []
        startup_idx = service.getAttribute("startup_idx")
        if startup_idx is not None:
            values.append("startidx=%s" % startup_idx)
        startup_time = service.getAttribute("start_time")
        if startup_time is not None:
            values.append("starttime=%s" % startup_time)
        dirs = []
        for dir in service.getElementsByTagName("Directory"):
            dirname = dir.getAttribute("name")
            dirs.append(dirname)
        if len(dirs):
            values.append("dirs=%s" % dirs)

        startup = []
        shutdown = []
        validate = []
        for cmd in service.getElementsByTagName("Command"):
            type = cmd.getAttribute("type")
            cmdstr = xmlutils.get_text_child(cmd)
            if cmdstr is None:
                continue
            if type == "start":
                startup.append(cmdstr)
            elif type == "stop":
                shutdown.append(cmdstr)
            elif type == "validate":
                validate.append(cmdstr)
        if len(startup):
            values.append("cmdup=%s" % startup)
        if len(shutdown):
            values.append("cmddown=%s" % shutdown)
        if len(validate):
            values.append("cmdval=%s" % validate)

        files = []
        for file in service.getElementsByTagName("File"):
            filename = file.getAttribute("name")
            files.append(filename)
            data = xmlutils.get_text_child(file)
            typestr = "service:%s:%s" % (name, filename)
            self.session.services.setservicefile(nodenum=n.objid, type=typestr,
                                                 filename=filename,
                                                 srcname=None, data=data)
        if len(files):
            values.append("files=%s" % files)
        if not bool(service.getAttribute("custom")):
            return True
        self.session.services.setcustomservice(n.objid, svc, values)
        return True

    def parsehooks(self, hooks):
        ''' Parse hook scripts from XML into session._hooks.
        '''
        for hook in hooks.getElementsByTagName("Hook"):
            filename = hook.getAttribute("name")
            state = hook.getAttribute("state")
            data = xmlutils.get_text_child(hook)
            if data is None:
                data = ""  # allow for empty file
            type = "hook:%s" % state
            self.session.set_hook(type, file_name=filename, source_name=None, data=data)

    def parsemeta(self):
        opt = xmlutils.get_one_element(self.meta, "SessionOptions")
        if opt:
            for param in opt.getElementsByTagName("param"):
                k = str(param.getAttribute("name"))
                v = str(param.getAttribute("value"))
                if v == '':
                    v = xmlutils.get_text_child(param)  # allow attribute/text for newlines
                setattr(self.session.options, k, v)
        hooks = xmlutils.get_one_element(self.meta, "Hooks")
        if hooks:
            self.parsehooks(hooks)
        meta = xmlutils.get_one_element(self.meta, "MetaData")
        if meta:
            for param in meta.getElementsByTagName("param"):
                k = str(param.getAttribute("name"))
                v = str(param.getAttribute("value"))
                if v == '':
                    v = xmlutils.get_text_child(param)
                self.session.metadata.add_item(k, v)