# Class describing NID
#
# Copyright (C) 2011 Michal Kajan, Libor Polčák, Petr Kramoliš, Radek Hranický
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import IPy
from ast import literal_eval

import re

""" Module containing class hierarchy describing supported NIDs.

If possible do not create objects directly but use CreateNID function.
"""

# IEEE802-like convention
mac_group2 = r"(?:[0-9a-fA-F]){1,2}"
mac_addr2 = r"(?P<ieee>" + mac_group2 + r"(?P<ieeediv>[-:])(?:" + mac_group2 + r"(?P=ieeediv)){4}" + mac_group2 + ")"
# Cisco-like convention
mac_group4 = r"(?:[0-9a-fA-F]){4}"
mac_addr4 = r"(?P<cisco>" + mac_group4 + r"\." + mac_group4 + r"\." + mac_group4 + ")"
# Supported MAC address format
mac_addr = re.compile(r"^" + mac_addr2 + r"|" + mac_addr4 + r"$")

class NID(object):
    """ Abstract class """

    def __init__(self, value):
        """ Constructor

        value NID value
        """
        if self.__class__ == NID:
            raise NotImplementedError("Cannot create instance of abstract class")
        self._value = value

    def getValue(self):
        return self._value

    def getSubNIDs(self):
        """ In case of NIDs composed of other NIDs, returns an iterable over the subNIDs.

        In case of atomic NIDs, returns empty list.
        """
        ret = []
        try:
            for s in self._value:
                if isinstance(s, NID):
                    ret.append(s)
                    ret += s.getSubNIDs()
        except TypeError: # self._value is not an iterable
            pass
        return ret

    @staticmethod
    def getType():
        raise NotImplementedError("Cannot call abstract method")
        
    @staticmethod
    def getIdentifierType():
        raise NotImplementedError("Cannot call abstract method")
        
    @staticmethod
    def isNIDCC():
        raise NotImplementedError("Cannot call abstract method")

    def __contains__(self, item):
        raise NotImplementedError("Cannot call abstract method")

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        else:
            return (other.getValue() == self.getValue())

    def __hash__(self):
        return hash(self.getValue())

    def __str__(self):
        """ You can recreate the NID object x by calling CreateNID(str(x)) """
        raise NotImplementedError("Cannot call abstract method")

    def __repr__(self):
        """ You can recreate the NID object x by calling CreateNID(eval(repr(x))) """
        return repr(str(self))


class NIDIPv4(NID):

    def __init__(self, value):
        """ Extends """
        if value.version() == 4:
            NID.__init__(self, value)
        else:
            raise NotImplementedError("%s is not an IPv4 address" % (str(value)))

    @staticmethod
    def getType():
        return "IPv4"
        
    @staticmethod
    def getIdentifierType():
        return "B"

    @staticmethod
    def isNIDCC():
        return True

    def __contains__(self, item):
        if item.getType() == "IPv4":
            return item.getValue() in self.getValue()
        elif item.getType() == "TCP":
            return (item.getClientIP() in self) or (item.getServerIP() in self)
        elif item.getType() == "TCP3":
            return item.getIP() in self
        else:
            return False

    def __str__(self):
        return str(self._value)

class NIDIPv6(NID):

    def __init__(self, value):
        """ Extends """
        if value.version() == 6:
            NID.__init__(self, value)
        else:
            raise NotImplementedError("%s is not an IPv4 address" % (str(value)))

    @staticmethod
    def getType():
        return "IPv6"
        
    @staticmethod
    def getIdentifierType():
        return "B"
        
    @staticmethod
    def isNIDCC():
        return True

    def __contains__(self, item):
        if item.getType() == "IPv6":
            return item.getValue() in self.getValue()
        elif item.getType() == "TCP":
            return (item.getClientIP() in self) or (item.getServerIP() in self)
        elif item.getType() == "TCP3":
            return item.getIP() in self
        else:
            return False

    def __str__(self):
        return str(self._value)


class NIDMAC(NID):

    def __init__(self, value):
        lower_val = value.lower()
        parsed = mac_addr.match(lower_val)
        if parsed:
            c = []
            if parsed.groupdict()["ieee"]:
                s = parsed.groupdict()["ieee"].split(parsed.groupdict()["ieeediv"])
                for n in s:
                    if len(n) < 2:
                        c.append("0" + n)
                    else:
                        c.append(n)
            elif parsed.groupdict()["cisco"]:
                s = parsed.groupdict()["cisco"]
                c = [s[0:2], s[2:4], s[5:7], s[7:9], s[10:12], s[12:14]]
            m = ":".join(c)
            NID.__init__(self, m)
        else:
            raise ValueError("Unknown MAC address format " + value)

    @staticmethod
    def getType():
        return "MAC"
        
    @staticmethod
    def getIdentifierType():
        return "C"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return self._value

class NIDPPPLogin(NID):

    @staticmethod
    def getType():
        return "PPP Login"
        
    @staticmethod
    def getIdentifierType():
        return "D"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "ppp:%s" % str(self._value)


class NIDRADIUSLogin(NID):

    @staticmethod
    def getType():
        return "Radius Login"
        
    @staticmethod
    def getIdentifierType():
        return "D"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "radius:%s" % str(self._value)

class NIDPPPSession(NID):

    @staticmethod
    def getType():
        return "PPP Session"
        
    @staticmethod
    def getIdentifierType():
        return "D"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "ppps:%s" % str(self._value)

class NIDDUID(NID):

    @staticmethod
    def getType():
        return "DUID"
        
    @staticmethod
    def getIdentifierType():
        return "C"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "duid:%s" % str(self._value)

class NIDDHCPClientID(NID):

    @staticmethod
    def getType():
        return "DHCP client ID"
        
    @staticmethod
    def getIdentifierType():
        return "C"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "dhcpClientID:%s" % str(self._value)


class NIDYMSGLogin(NID):

    @staticmethod
    def getType():
        return "YMSG LOGIN"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "ymsgLogin:%s" % str(self._value)

class NIDOSCARLogin(NID):

    @staticmethod
    def getType():
        return "OSCAR LOGIN"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "oscarLogin:%s" % str(self._value)

class NIDIRCLogin(NID):

    @staticmethod
    def getType():
        return "IRC LOGIN"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "ircLogin:%s" % str(self._value)

class NIDXMPPLogin(NID):

    @staticmethod
    def getType():
        return "XMPP LOGIN"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "xmppLogin:%s" % str(self._value)

class NIDIRCChannel(NID):

    @staticmethod
    def getType():
        return "IRC CHANNEL"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "ircChannel:%s" % str(self._value)


class NIDWisLogin(NID):

    @staticmethod
    def getType():
        return "WIS login"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "wis-login:%s" % str(self._value)


class NIDSDNLocation(NID):

    @staticmethod
    def getType():
        return "SDN location"

    @staticmethod
    def getIdentifierType():
        return "A"

    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "sdn-location:%s" % str(self._value)


class NIDEmailAddress(NID):

    @staticmethod
    def getType():
        return "E-mail address"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item #FIXME

    def __str__(self):
        return "email:%s" % str(self._value)

class NIDEmailMsgID(NID):

    @staticmethod
    def getType():
        return "E-mail Message-ID"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item

    def __str__(self):
        return "emailmsgid:%s" % str(self._value)
        

class NIDEmailMessage(NID):

    def __init__(self, value):
        """ Extends """
        if (type(value) != tuple):
            # In case we use CreateNID('emailmsg:(...)')
            value = literal_eval(value)

        val = []
        val.append(CreateNIDType(value[0][1], "E-mail address"))
        val.append(CreateNIDType(value[2][1], "E-mail Message-ID"))
        for rcpt in value[1]:
            val.append(CreateNIDType(rcpt[1], "E-mail address"))
        NID.__init__(self, tuple(val))
        
    @staticmethod
    def getType():
        return "E-mail message"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False
        
    def __hash__(self):
        return hash(str(self.getValue()))
        
    def getSndEmail(self):
        return self._value[0]
        
    def getRcptEmails(self):
        return self._value[2:]
        
    def getMsgID(self):
        return self._value[1]

    def __contains__(self, item):
        return self == item #FIXME

    def __str__(self):
        return "emailmsg:%s" % str((self._value[0], self._value[2], self._value[1]))


class NIDSIP(NID):

    @staticmethod
    def getType():
        return "SIP URI"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item #FIXME

    def __str__(self):
        return "sip:%s" % str(self._value)

class NIDRTP(NID):

    @staticmethod
    def getType():
        return "RTP PORT"
        
    @staticmethod
    def getIdentifierType():
        return "A"
        
    @staticmethod
    def isNIDCC():
        return False

    def __contains__(self, item):
        return self == item #FIXME

    def __str__(self):
        return "rtp:%s" % str(self._value)

class NIDL4(NID):
    """ Abstract class """

    def __init__(self, value):
        """ Constructor

        value NID value
        """
        if self.__class__ == NIDL4:
            raise NotImplementedError("Cannot create instance of abstract class")
        NID.__init__(self, value)

    @staticmethod
    def getIdentifierType():
        return "A"

    @staticmethod
    def isNIDCC():
        return True

class NIDL4Flow(NIDL4):
    """ Abstract class """

    def __init__(self, value):
        """ Constructor

        value NID value
        """
        if self.__class__ == NIDL4Flow:
            raise NotImplementedError("Cannot create instance of abstract class")
        NIDL4.__init__(self, self._parseRepr(value))

    def getClientIP(self):
        return self.getValue()[0]

    def getClientPort(self):
        return self.getValue()[1]

    def getServerIP(self):
        return self.getValue()[2]

    def getServerPort(self):
        return self.getValue()[3]

    def _parseRepr(self, value):
        if (type(value) == tuple):
            # In case pf CreateNIDType(('1.1.1.1', 1, '2.2.2.2', 2), "PROTO")
            ret = (NIDIP(value[0]), value[1], NIDIP(value[2]), value[3])
        else:
            # In case of CreateNID('proto:(\'1.1.1.1\', 1, \'2.2.2.2\', 2)')            
            if value[0] == '(' and value[len(value)-1] == ')':
                value = value[1:len(value)-1]
            value = value.replace("\"", "")
            value = value.replace("'", "")
            value = value.replace(" ", "")
            ret = value.split(',')
            ret[0] = NIDIP(ret[0])
            ret[1] = int(ret[1])
            ret[2] = NIDIP(ret[2])
            ret[3] = int(ret[3])
            ret = tuple(ret)
        return ret

#[('TCP', ('10.0.2.15', 50329, '88.86.102.50', 5222))]
class NIDTCP(NIDL4Flow):
  
    def __init__(self, value):
        """ Extends """
        NIDL4Flow.__init__(self, value)

    @staticmethod
    def getType():
        return "TCP"

    def __contains__(self, item):
        return self == item

    def __str__(self):
        #return "tcp:%s" % str(self._value)
        return "tcp:(%s, %d, %s, %d)" % (self.getClientIP(), self.getClientPort(),
                self.getServerIP(), self.getServerPort())


#[('TCP3', '10.0.2.15_50329')]
class NIDTCP3(NIDL4):

    def __init__(self, value):
        """ Extends """
        ip, port = value.split("_")
        self.__ip = NIDIP(ip)
        self.__port = int(port)
        NIDL4.__init__(self, (self.__ip, self.__port))

    @staticmethod
    def getType():
        return "TCP3"

    @staticmethod
    def getIdentifierType():
        return "A"

    @staticmethod
    def isNIDCC():
        return True
        
    def getIP(self):
        return self.__ip
        
    def getPort(self):
        return self.__port

    def __contains__(self, item):
        if item.getType() == "TCP":
            return ((item.getClientIP() in self.__ip and self.__port == item.getClientPort()) or 
                    (item.getServerIP() in self.__ip and self.__port == item.getServerPort()))
        else:
            return self == item

    def __str__(self):
        return "tcp3:%s_%d" % (self.__ip, self.__port)

#[('UDP', ('10.0.2.15', 50329, '88.86.102.50', 5222))]
class NIDUDP(NIDL4Flow):
  
    def __init__(self, value):
        """ Extends """
        NIDL4Flow.__init__(self, value)

    @staticmethod
    def getType():
        return "UDP"

    def __contains__(self, item):
        return self == item

    def __str__(self):
        #return "tcp:%s" % str(self._value)
        return "udp:(%s, %d, %s, %d)" % (self.getClientIP(), self.getClientPort(),
                self.getServerIP(), self.getServerPort())


#[('UDP3', '10.0.2.15_50329')]
class NIDUDP3(NIDL4):

    def __init__(self, value):
        """ Extends """
        ip, port = value.split("_")
        self.__ip = NIDIP(ip)
        self.__port = int(port)
        NIDL4.__init__(self, (self.__ip, self.__port))

    @staticmethod
    def getType():
        return "UDP3"

    @staticmethod
    def getIdentifierType():
        return "A"

    @staticmethod
    def isNIDCC():
        return True
        
    def getIP(self):
        return self.__ip
        
    def getPort(self):
        return self.__port

    def __contains__(self, item):
        if item.getType() == "UDP":
            return ((item.getClientIP() in self.__ip and self.__port == item.getClientPort()) or 
                    (item.getServerIP() in self.__ip and self.__port == item.getServerPort()))
        else:
            return self == item

    def __str__(self):
        return "udp3:%s_%d" % (self.__ip, self.__port)

#[('sdnConnector', 'OF_00:00:00:00:00:00:00:03_1')]
class sdnConnector(NID):
    def __init__(self, value):
        """ Extends """
        node_type, node_id, node_connector_id = value.split("_")
        self.__node_type = node_type
        self.__node_id = node_id
        self.__node_connector_id = int(node_connector_id)
        NID.__init__(self, value)

    @staticmethod
    def getType():
        return "sdnConnector"
        
    @staticmethod
    def getIdentifierType():
        return "C"
        
    @staticmethod
    def isNIDCC():
        return False
    
    @staticmethod
    def getNodeType(self):
        return self.__node_type
    
    @staticmethod
    def getNodeId(self):
        return self.__node_id
    
    @staticmethod
    def getConnectorId(self):
        return self.__node_connector_id

    def __contains__(self, item):
        if item.getType() == "sdnConnector":
            return (item.__node_type == self.__node_type and item.__node_id == self.__node_id and
                    item.__node_connector_id == self.__node_connector_id)
        else:
            return self == item

    def __str__(self):
        return "sdnConnector:%s_%s_%s" % (self.__node_type, self.__node_id, str(self.__node_connector_id))


# IP addresses are not part of the dict in order not to create them directly but to call NIDIP
nid_classes = {}
for c in [NIDMAC, NIDRADIUSLogin, NIDPPPLogin, NIDPPPSession, NIDDUID,
        NIDDHCPClientID, NIDTCP, NIDTCP3, NIDUDP, NIDUDP3, NIDIRCChannel, NIDYMSGLogin,
        NIDIRCLogin, NIDXMPPLogin, NIDOSCARLogin, NIDWisLogin, NIDSDNLocation, NIDEmailAddress,
        NIDEmailMsgID, NIDEmailMessage, NIDSIP, NIDRTP, sdnConnector]:
    nid_classes[c.getType()] = c


def NIDIP(ip):
    """ Makes NID ip address from a string """
    ipy = IPy.IP(ip)
    v = ipy.version()
    return {4: NIDIPv4, 6: NIDIPv6}[v](ipy)

def CreateNID(nid):
    """ Translate text representation of a NID to a supported object """
    try:
        p = nid.find(":")
    except AttributeError:        
        try:
            version = nid.version()
            return {4: NIDIPv4, 6: NIDIPv6}[version](nid)
        except AttributeError:
            raise NotImplementedError("Unkonown IP version %s" % version)
    prefixed_nids = {"radius": NIDRADIUSLogin, "ppps": NIDPPPSession, "ppp":NIDPPPLogin,
                      "duid":NIDDUID, "dhcpClientID":NIDDHCPClientID, "tcp":NIDTCP, "tcp3":NIDTCP3,
                      "udp": NIDUDP, "udp3": NIDUDP3,
                      "ircChannel":NIDIRCChannel, "ymsgLogin":NIDYMSGLogin,
                      "ircLogin": NIDIRCLogin, "xmppLogin": NIDXMPPLogin, "oscarLogin":NIDOSCARLogin,
                      "wis-login": NIDWisLogin, "sdn-location": NIDSDNLocation, "email": NIDEmailAddress, "emailmsgid": NIDEmailMsgID, "emailmsg": NIDEmailMessage,
                      "sip": NIDSIP, "rtp": NIDRTP, "sdnConnector": sdnConnector}
    prefix = nid[:p]
    if prefix in prefixed_nids.keys():
        return prefixed_nids[prefix](nid[p+1:])
    try:
        return NIDIP(nid)
    except ValueError:
        pass
    try:
        return NIDMAC(nid)
    except ValueError as e:
        pass
    raise NotImplementedError("Unknown NID %s" % nid)

def CreateNIDType(nid, t):
    """ Translate text string to a NID object of given type """
    if t in nid_classes:
        return nid_classes[t](nid)
    else:
        try:
            return NIDIP(nid)
        except ValueError:
            pass
    raise NotImplementedError("Unknown NID %s of type %s" % (nid, t))
