#    Copyright (c) 2003, Nullcube Pty Ltd 
#    All rights reserved.
#
#    Redistribution and use in source and binary forms, with or without
#    modification, are permitted provided that the following conditions are met:
#
#    *   Redistributions of source code must retain the above copyright notice, this
#        list of conditions and the following disclaimer.
#    *   Redistributions in binary form must reproduce the above copyright notice,
#        this list of conditions and the following disclaimer in the documentation
#        and/or other materials provided with the distribution.
#    *   Neither the name of Nullcube nor the names of its contributors may be used to
#        endorse or promote products derived from this software without specific
#        prior written permission.
#
#    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
#    ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
#    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
#    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
#    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
    A Python module for querying and manipulating network interfaces.
"""
#    TODO:
#        - Wireless network operations.

import _ifconfig, _sysvar, utils

IfConfigError = _ifconfig.IfConfigError

AF_INET             = _sysvar.AF_INET
AF_INET6            = _sysvar.AF_INET6
AF_LINK             = _sysvar.AF_LINK

IFF_UP              = _ifconfig.IFF_UP
IFF_BROADCAST       = _ifconfig.IFF_BROADCAST
IFF_DEBUG           = _ifconfig.IFF_DEBUG
IFF_LOOPBACK        = _ifconfig.IFF_LOOPBACK
IFF_POINTOPOINT     = _ifconfig.IFF_POINTOPOINT
IFF_NOTRAILERS      = _ifconfig.IFF_NOTRAILERS
IFF_RUNNING         = _ifconfig.IFF_RUNNING
IFF_NOARP           = _ifconfig.IFF_NOARP
IFF_PROMISC         = _ifconfig.IFF_PROMISC
IFF_ALLMULTI        = _ifconfig.IFF_ALLMULTI
IFF_OACTIVE         = _ifconfig.IFF_OACTIVE
IFF_SIMPLEX         = _ifconfig.IFF_SIMPLEX
IFF_LINK0           = _ifconfig.IFF_LINK0
IFF_LINK1           = _ifconfig.IFF_LINK1
IFF_LINK2           = _ifconfig.IFF_LINK2
IFF_MULTICAST       = _ifconfig.IFF_MULTICAST


def unique(lst):
    vals = {}
    for i in lst:
        vals[i] = 0
    return vals.keys()

    
class FlagVal(int):
    _flags = [
        (IFF_UP,            "UP"),
        (IFF_BROADCAST,     "BROADCAST"),
        (IFF_DEBUG,         "DEBUG"),
        (IFF_LOOPBACK,      "LOOPBACK"),
        (IFF_POINTOPOINT,   "POINTOPOINT"),
        (IFF_NOTRAILERS,    "NOTRAILERS"),
        (IFF_RUNNING,       "RUNNING"),
        (IFF_NOARP,         "NOARP"),
        (IFF_PROMISC,       "PROMISC"),
        (IFF_ALLMULTI,      "ALLMULTI"),
        (IFF_OACTIVE,       "OACTIVE"),
        (IFF_SIMPLEX,       "SIMPLEX"),
        (IFF_LINK0,         "LINK0"),
        (IFF_LINK1,         "LINK1"),
        (IFF_LINK2,         "LINK2"),
        (IFF_MULTICAST,     "MULTICAST"),
    ]
    def flagdesc(self):
        strs = []
        for i in self._flags:
            if self&i[0]:
                strs.append(i[1])
        return "|".join(strs)

    def __str__(self):
        return "%x<%s>"%(self, self.flagdesc())


class Flags(object):
    def __get__(self, obj, val):
        return FlagVal(obj._getinfo()["flags"])

    def __set__(self, obj, val):
        obj._setflags(val)


class MTU(object):
    def __get__(self, obj, val):
        return obj._getinfo()["mtu"]

    def __set__(self, obj, val):
        obj._setmtu(val)


class Metric(object):
    def __get__(self, obj, val):
        return obj._getinfo()["metric"]

    def __set__(self, obj, val):
        obj._setmetric(val)


class Media(object):
    """
        Class representing the media for a single interface. Interface media
        have the following attributes:

            mtype       The media type. This is static for an interface, and cannot
                        be changed. E.g. "Ethernet"

            subtype     This is the value we modify to change the media for an
                        interface. E.g. "10baseT"

            options     Every subtype has a set of options. E.g. "Full Duplex"
    """
    def __init__(self, interface):
        self._interface = interface

    def __repr__(self):
        return "media: %s %s"%(self.mtype, self.subtype)

    def _getType(self):
        return _ifconfig.getifmedia(self._interface)["current"][0]

    def _getSubType(self):
        return _ifconfig.getifmedia(self._interface)["current"][1]

    def _getOptions(self):
        return _ifconfig.getifmedia(self._interface)["current"][2]

    def _getActiveSubType(self):
        return _ifconfig.getifmedia(self._interface)["active"][1]

    def _getActiveOptions(self):
        return _ifconfig.getifmedia(self._interface)["active"][2]
    
    def getAllSubtypes(self):
        """
            Retrieve all possible subtypes for this interface.
        """
        return unique([i[1] for i in _ifconfig.getifmedia(self._interface)["options"]])

    def getAllOptions(self, subtype):
        """
            Retrieve all possible options for a given subtype.
        """
        options = []
        for i in _ifconfig.getifmedia(self._interface)["options"]:
            if i[1] == subtype:
                options.extend(i[2])
        return options

    mtype = property(_getType, None, None)
    subtype = property(_getSubType, None, None)
    options = property(_getOptions, None, None)
    active_subtype = property(_getActiveSubType, None, None)
    active_options = property(_getActiveOptions, None, None)


class Interface(object):
    """
        Each interface contains the following information:
            - Interface Flags
            - Media information
            - A list of addresses, each with associated information.
    """
    Iftype = "unknown"
    flags = Flags()
    mtu = MTU()
    metric = Metric()
    _addrTypeLookup = {
        AF_INET:    "inet",
        AF_INET6:   "inet6",
        AF_LINK:    "link"
    }
    def __init__(self, name):
        self.Name = name
        try:
            _ifconfig.getifmedia(self.Name)
            self.media = Media(self.Name)
        except _ifconfig.IfConfigError:
            self.media = None
        # We de-reference this once, so we get the Iftype
        self.addresses

    def _getinfo(self):
        return _ifconfig.getifinfo(self.Name)

    def _setflags(self, val):
        _ifconfig.setifflags(self.Name, val)

    def _setmtu(self, val):
        _ifconfig.setifmtu(self.Name, val)

    def _setmetric(self, val):
        _ifconfig.setifmetric(self.Name, val)

    def _addrToStr(self, addr, af):
        if af == AF_INET:
            return (utils.ipToStr(addr))
        elif af == AF_INET6:
            return (utils.ip6ToStr(addr))
        elif af == AF_LINK:
            return (utils.ethToStr(addr))
        else:
            return addr

    def _getAddresses(self):
        addrlist = _ifconfig.getifaddrs()
        addresses = []
        for i in addrlist:
            if i["name"] == self.Name:
                if i["address"].has_key("iftype"):
                    # This is a link-layer address. 
                    self.Iftype = i["address"]["iftype"]
                if i["address"]["address"]:
                    del i["name"]
                    for j in i.keys():
                        i[j]["address"] = self._addrToStr( i[j]["address"], i[j]["sa_family"])
                    addresses.append(i)
        return addresses

    def __repr__(self):
        s = "%s: flags=%s mtu %s"%(self.Name, self.flags, self.mtu)
        addrs = [""]
        if self.media:
            addrs.append("\t media: %s %s"%(self.media.mtype, self.media.subtype))
        for i in self.addresses:
            atype = self._addrTypeLookup.get(i["address"]["sa_family"], "unknown")
            addrs.append("\t %s: %s"%(atype, i["address"]["address"]))
        return s + "\n".join(addrs)

    def addAddress(self, addr, mask=None):
        """
            Add an address to this interface. At the moment only IP and IPv6
            are supported.

            The mask specification can either be an address (i.e. 255.255.0.0),
            or a numeric prefix (i.e. 16).

            The default masks are 24 for IPv4 and 64 for IPv6.
        """
        try:
            int(mask)
            numericmask = 1
        except (TypeError, ValueError):
            numericmask = 0
        if utils.isIPAddr(addr):
            bytes = utils.ipToBytes(addr)
            af = AF_INET
            if mask is None:
                mask = "255.255.255.0"
            if numericmask:
                mask = utils.ipFromPrefix(mask)
            mask = utils.ipToBytes(mask)
        elif utils.isIP6Addr(addr):
            bytes = utils.ip6ToBytes(addr)
            af = AF_INET6
            if mask is None:
                mask = 64
                numericmask = 1
            if numericmask:
                mask = utils.ip6FromPrefix(mask)
            mask = utils.ip6ToBytes(mask)
        else:
            raise ValueError, "Invalid address."
        _ifconfig.addaddr(self.Name, af, bytes, mask)

    def delAddress(self, addr):
        """
            Remove an address from this interface. At the moment only IP and
            IPv6 are supported.
        """
        if utils.isIPAddr(addr):
            bytes = utils.ipToBytes(addr)
            af = AF_INET
        elif utils.isIP6Addr(addr):
            bytes = utils.ip6ToBytes(addr)
            af = AF_INET6
        else:
            raise ValueError, "Invalid address."
        _ifconfig.deladdr(self.Name, af, bytes)

    def setAddress(self, addr, mask=None):
        """
            Change an interface address.

            This function operates as follows:
                - If are existing addresses of the same type as the specified
                  address (i.e. IPv4 or IPv6), remove the first address found.
                - Then add the specified address to the interface.
        """
        if utils.isIPAddr(addr):
            v4 = 1
        elif utils.isIP6Addr(addr):
            v4 = 0
        else:
            raise ValueError, "Invalid address."
        for i in self.addresses:
            if v4:
                if utils.isIPAddr(i["address"]["address"]):
                    self.delAddress(i["address"]["address"])
                    break
            else:
                if utils.isIP6Addr(i["address"]["address"]):
                    self.delAddress(i["address"]["address"])
                    break
        self.addAddress(addr, mask)

    addresses = property(_getAddresses, None, None)


class IFConfig(object):
    def _getInterfaces(self):
        interfaces = {}
        addrlist = _ifconfig.getifaddrs()
        for i in addrlist:
            if not interfaces.has_key(i["name"]):
                interfaces[i["name"]] = Interface(i["name"])
        return interfaces

    def __getitem__(self, item):
        return self.interfaces.__getitem__(item)

    def has_key(self, item):
        return self.interfaces.has_key(item)

    def keys(self):
        return self.interfaces.keys()

    def create(self, ifname):
        _ifconfig.create(ifname)

    def destroy(self, ifname):
        _ifconfig.destroy(ifname)

    def __repr__(self):
        out = []
        ifaces = self.interfaces.keys()
        ifaces.sort()
        for i in ifaces:
            out.append(repr(self.interfaces[i]))
        return "\n".join(out)

    interfaces = property(_getInterfaces, None, None)
