from xml.dom.minidom import parse

from core import logger
from core.conf import ConfigShim
from core.enumerations import NodeTypes
from core.misc import nodeutils
from core.service import ServiceManager, ServiceShim
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 = obj.getAttribute("id")
        try:
            node_id = int(node_id)
        except:
            logger.debug("parsing node without integer id: %s", node_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:
            for k, v in kvs:
                mgr.set_config(k, v, node_id=nodenum, config_type=name)

    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(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 and scale:
            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.default_services[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)
            services = svclists[objid]
            if services:
                services = services.split("|")
            self.session.services.add_services(node=n, node_type=n.type, services=services)

    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)
            self.session.services.set_service_file(node_id=n.objid, service_name=name, file_name=filename, data=data)

        if len(files):
            values.append("files=%s" % files)
        if not bool(service.getAttribute("custom")):
            return True
        self.session.services.set_service(n.objid, svc)
        # set custom values for custom service
        svc = self.session.services.get_service(n.objid, None)
        if not svc:
            raise ValueError("custom service(%s) for node(%s) does not exist", svc.name, n.objid)
        values = ConfigShim.str_to_dict("|".join(values))
        for name, value in values.iteritems():
            ServiceShim.setvalue(svc, name, value)
        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
                self.session.options.set_config(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.set_config(k, v)
