#
# Netlink interface based on libnl
#
# Copyright (c) 2011 Thomas Graf <tgraf@suug.ch>
#

"""netlink library based on libnl

This module provides an interface to netlink sockets

The module contains the following public classes:
 - Socket -- The netlink socket
 - Message -- The netlink message
 - Callback -- The netlink callback handler
 - Object -- Abstract object (based on struct nl_obect in libnl) used as
         base class for all object types which can be put into a Cache
 - Cache -- A collection of objects which are derived from the base
        class Object. Used for netlink protocols which maintain a list
        or tree of objects.
 - DumpParams --

The following exceptions are defined:
 - NetlinkError -- Base exception for all general purpose exceptions raised.
 - KernelError -- Raised when the kernel returns an error as response to a
          request.

All other classes or functions in this module are considered implementation
details.
"""
from __future__ import absolute_import


from . import capi
import sys
import socket

__all__ = [
    "Socket",
    "Message",
    "Callback",
    "DumpParams",
    "Object",
    "Cache",
    "KernelError",
    "NetlinkError",
]

__version__ = "0.1"

# netlink protocols
NETLINK_ROUTE = 0
# NETLINK_UNUSED = 1
NETLINK_USERSOCK = 2
NETLINK_FIREWALL = 3
NETLINK_INET_DIAG = 4
NETLINK_NFLOG = 5
NETLINK_XFRM = 6
NETLINK_SELINUX = 7
NETLINK_ISCSI = 8
NETLINK_AUDIT = 9
NETLINK_FIB_LOOKUP = 10
NETLINK_CONNECTOR = 11
NETLINK_NETFILTER = 12
NETLINK_IP6_FW = 13
NETLINK_DNRTMSG = 14
NETLINK_KOBJECT_UEVENT = 15
NETLINK_GENERIC = 16
NETLINK_SCSITRANSPORT = 18
NETLINK_ECRYPTFS = 19

NL_DONTPAD = 0
NL_AUTO_PORT = 0
NL_AUTO_SEQ = 0

NL_DUMP_LINE = 0
NL_DUMP_DETAILS = 1
NL_DUMP_STATS = 2

NLM_F_REQUEST = 1
NLM_F_MULTI = 2
NLM_F_ACK = 4
NLM_F_ECHO = 8

NLM_F_ROOT = 0x100
NLM_F_MATCH = 0x200
NLM_F_ATOMIC = 0x400
NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH

NLM_F_REPLACE = 0x100
NLM_F_EXCL = 0x200
NLM_F_CREATE = 0x400
NLM_F_APPEND = 0x800


class NetlinkError(Exception):
    def __init__(self, error):
        self._error = error
        self._msg = capi.nl_geterror(error)

    def __str__(self):
        return self._msg


class KernelError(NetlinkError):
    def __str__(self):
        return "Kernel returned: {0}".format(self._msg)


class ImmutableError(NetlinkError):
    def __init__(self, msg):
        self._msg = msg

    def __str__(self):
        return "Immutable attribute: {0}".format(self._msg)


class Message(object):
    """Netlink message"""

    def __init__(self, size=0):
        if size == 0:
            self._msg = capi.nlmsg_alloc()
        else:
            self._msg = capi.nlmsg_alloc_size(size)

        if self._msg is None:
            raise Exception("Message allocation returned NULL")

    def __del__(self):
        capi.nlmsg_free(self._msg)

    def __len__(self):
        return capi.nlmsg_len(capi.nlmsg_hdr(self._msg))

    @property
    def protocol(self):
        return capi.nlmsg_get_proto(self._msg)

    @protocol.setter
    def protocol(self, value):
        capi.nlmsg_set_proto(self._msg, value)

    @property
    def maxSize(self):
        return capi.nlmsg_get_max_size(self._msg)

    @property
    def hdr(self):
        return capi.nlmsg_hdr(self._msg)

    @property
    def data(self):
        return capi.nlmsg_data(self._msg)

    @property
    def attrs(self):
        return capi.nlmsg_attrdata(self._msg)

    def send(self, sock):
        sock.send(self)


class Callback(object):
    """Netlink callback"""

    def __init__(self, kind=capi.NL_CB_DEFAULT):
        if isinstance(kind, Callback):
            self._cb = capi.py_nl_cb_clone(kind._cb)
        else:
            self._cb = capi.nl_cb_alloc(kind)

    def __del__(self):
        capi.py_nl_cb_put(self._cb)

    def set_type(self, t, k, handler, obj):
        return capi.py_nl_cb_set(self._cb, t, k, handler, obj)

    def set_all(self, k, handler, obj):
        return capi.py_nl_cb_set_all(self._cb, k, handler, obj)

    def set_err(self, k, handler, obj):
        return capi.py_nl_cb_err(self._cb, k, handler, obj)

    def clone(self):
        return Callback(self)


class Socket(object):
    """Netlink socket"""

    def __init__(self, cb=None):
        if isinstance(cb, Callback):
            self._sock = capi.nl_socket_alloc_cb(cb._cb)
        elif cb is None:
            self._sock = capi.nl_socket_alloc()
        else:
            raise Exception("'cb' parameter has wrong type")

        if self._sock is None:
            raise Exception("NULL pointer returned while allocating socket")

    def __del__(self):
        capi.nl_socket_free(self._sock)

    def __str__(self):
        return "nlsock<{0}>".format(self.local_port)

    @property
    def local_port(self):
        return capi.nl_socket_get_local_port(self._sock)

    @local_port.setter
    def local_port(self, value):
        capi.nl_socket_set_local_port(self._sock, int(value))

    @property
    def peer_port(self):
        return capi.nl_socket_get_peer_port(self._sock)

    @peer_port.setter
    def peer_port(self, value):
        capi.nl_socket_set_peer_port(self._sock, int(value))

    @property
    def peer_groups(self):
        return capi.nl_socket_get_peer_groups(self._sock)

    @peer_groups.setter
    def peer_groups(self, value):
        capi.nl_socket_set_peer_groups(self._sock, value)

    def set_bufsize(self, rx, tx):
        capi.nl_socket_set_buffer_size(self._sock, rx, tx)

    def connect(self, proto):
        capi.nl_connect(self._sock, proto)
        return self

    def disconnect(self):
        capi.nl_close(self._sock)

    def sendto(self, buf):
        ret = capi.nl_sendto(self._sock, buf, len(buf))
        if ret < 0:
            raise Exception("Failed to send")
        else:
            return ret

    def send_auto_complete(self, msg):
        if not isinstance(msg, Message):
            raise Exception("must provide Message instance")
        ret = capi.nl_send_auto_complete(self._sock, msg._msg)
        if ret < 0:
            raise Exception("send_auto_complete failed: ret=%d" % ret)
        return ret

    def recvmsgs(self, recv_cb):
        if not isinstance(recv_cb, Callback):
            raise Exception("must provide Callback instance")
        ret = capi.nl_recvmsgs(self._sock, recv_cb._cb)
        if ret < 0:
            raise Exception("recvmsg failed: ret=%d" % ret)


_sockets = {}


def lookup_socket(protocol):
    try:
        sock = _sockets[protocol]
    except KeyError:
        sock = Socket()
        sock.connect(protocol)
        _sockets[protocol] = sock

    return sock


class DumpParams(object):
    """Dumping parameters"""

    def __init__(self, type_=NL_DUMP_LINE):
        self._dp = capi.alloc_dump_params()
        if not self._dp:
            raise Exception("Unable to allocate struct nl_dump_params")

        self._dp.dp_type = type_

    def __del__(self):
        capi.free_dump_params(self._dp)

    @property
    def type(self):
        return self._dp.dp_type

    @type.setter
    def type(self, value):
        self._dp.dp_type = value

    @property
    def prefix(self):
        return self._dp.dp_prefix

    @prefix.setter
    def prefix(self, value):
        self._dp.dp_prefix = value


# underscore this to make sure it is deleted first upon module deletion
_defaultDumpParams = DumpParams(NL_DUMP_LINE)


class Object(object):
    """Cacheable object (base class)"""

    def __init__(self, obj_name, name, obj=None):
        self._obj_name = obj_name
        self._name = name
        self._modules = []

        if not obj:
            obj = capi.object_alloc_name(self._obj_name)

        self._nl_object = obj

        # Create a clone which stores the original state to notice
        # modifications
        clone_obj = capi.nl_object_clone(self._nl_object)
        self._orig = self._obj2type(clone_obj)

    def __del__(self):
        if not self._nl_object:
            raise ValueError()

        capi.nl_object_put(self._nl_object)

    def __str__(self):
        if hasattr(self, "format"):
            return self.format()
        else:
            return capi.nl_object_dump_buf(self._nl_object, 4096).rstrip()

    def _new_instance(self):
        raise NotImplementedError()

    def clone(self):
        """Clone object"""
        return self._new_instance(capi.nl_object_clone(self._nl_object))

    def _module_lookup(self, path, constructor=None):
        """Lookup object specific module and load it

        Object implementations consisting of multiple types may
        offload some type specific code to separate modules which
        are loadable on demand, e.g. a VLAN link or a specific
        queueing discipline implementation.

        Loads the module `path` and calls the constructor if
        supplied or `module`.init()

        The constructor/init function typically assigns a new
        object covering the type specific implementation aspects
        to the new object, e.g. link.vlan = VLANLink()
        """
        try:
            __import__(path)
        except ImportError:
            return

        module = sys.modules[path]

        if constructor:
            ret = getattr(module, constructor)(self)
        else:
            ret = module.init(self)

        if ret:
            self._modules.append(ret)

    def _module_brief(self):
        ret = ""

        for module in self._modules:
            if hasattr(module, "brief"):
                ret += module.brief()

        return ret

    def dump(self, params=None):
        """Dump object as human readable text"""
        if params is None:
            params = _defaultDumpParams

        capi.nl_object_dump(self._nl_object, params._dp)

    @property
    def mark(self):
        return bool(capi.nl_object_is_marked(self._nl_object))

    @mark.setter
    def mark(self, value):
        if value:
            capi.nl_object_mark(self._nl_object)
        else:
            capi.nl_object_unmark(self._nl_object)

    @property
    def shared(self):
        return capi.nl_object_shared(self._nl_object) != 0

    @property
    def attrs(self):
        attr_list = capi.nl_object_attr_list(self._nl_object, 1024)
        return attr_list[0].split()

    @property
    def refcnt(self):
        return capi.nl_object_get_refcnt(self._nl_object)

    # this method resolves multiple levels of sub types to allow
    # accessing properties of subclass/subtypes (e.g. link.vlan.id)
    def _resolve(self, attr):
        obj = self
        lst = attr.split(".")
        while len(lst) > 1:
            obj = getattr(obj, lst.pop(0))
        return (obj, lst.pop(0))

    def _setattr(self, attr, val):
        obj, attr = self._resolve(attr)
        return setattr(obj, attr, val)

    def _hasattr(self, attr):
        obj, attr = self._resolve(attr)
        return hasattr(obj, attr)


class ObjIterator(object):
    def __init__(self, cache, obj):
        self._cache = cache
        self._nl_object = None

        if not obj:
            self._end = 1
        else:
            capi.nl_object_get(obj)
            self._nl_object = obj
            self._first = 1
            self._end = 0

    def __del__(self):
        if self._nl_object:
            capi.nl_object_put(self._nl_object)

    def __iter__(self):
        return self

    def get_next(self):
        return capi.nl_cache_get_next(self._nl_object)

    def next(self):
        return self.__next__()

    def __next__(self):
        if self._end:
            raise StopIteration()

        if self._first:
            ret = self._nl_object
            self._first = 0
        else:
            ret = self.get_next()
            if not ret:
                self._end = 1
                raise StopIteration()

        # return ref of previous element and acquire ref of current
        # element to have object stay around until we fetched the
        # next ptr
        capi.nl_object_put(self._nl_object)
        capi.nl_object_get(ret)
        self._nl_object = ret

        # reference used inside object
        capi.nl_object_get(ret)
        return self._cache._new_object(ret)


class ReverseObjIterator(ObjIterator):
    def get_next(self):
        return capi.nl_cache_get_prev(self._nl_object)


class Cache(object):
    """Collection of netlink objects"""

    def __init__(self):
        if self.__class__ is Cache:
            raise NotImplementedError()
        self.arg1 = None
        self.arg2 = None

    def __del__(self):
        capi.nl_cache_free(self._nl_cache)

    def __len__(self):
        return capi.nl_cache_nitems(self._nl_cache)

    def __iter__(self):
        obj = capi.nl_cache_get_first(self._nl_cache)
        return ObjIterator(self, obj)

    def __reversed__(self):
        obj = capi.nl_cache_get_last(self._nl_cache)
        return ReverseObjIterator(self, obj)

    def __contains__(self, item):
        obj = capi.nl_cache_search(self._nl_cache, item._nl_object)
        if obj is None:
            return False
        else:
            capi.nl_object_put(obj)
            return True

    # called by sub classes to allocate type specific caches by name
    @staticmethod
    def _alloc_cache_name(name):
        return capi.alloc_cache_name(name)

    # implemented by sub classes, must return new instasnce of cacheable
    # object
    @staticmethod
    def _new_object(obj):
        raise NotImplementedError()

    # implemented by sub classes, must return instance of sub class
    def _new_cache(self, cache):
        raise NotImplementedError()

    def subset(self, filter_):
        """Return new cache containing subset of cache

        Cretes a new cache containing all objects which match the
        specified filter.
        """
        if not filter_:
            raise ValueError()

        c = capi.nl_cache_subset(self._nl_cache, filter_._nl_object)
        return self._new_cache(cache=c)

    def dump(self, params=None, filter_=None):
        """Dump (print) cache as human readable text"""
        if not params:
            params = _defaultDumpParams

        if filter_:
            filter_ = filter_._nl_object

        capi.nl_cache_dump_filter(self._nl_cache, params._dp, filter_)

    def clear(self):
        """Remove all cache entries"""
        capi.nl_cache_clear(self._nl_cache)

    # Called by sub classes to set first cache argument
    def _set_arg1(self, arg):
        self.arg1 = arg
        capi.nl_cache_set_arg1(self._nl_cache, arg)

    # Called by sub classes to set second cache argument
    def _set_arg2(self, arg):
        self.arg2 = arg
        capi.nl_cache_set_arg2(self._nl_cache, arg)

    def refill(self, socket=None):
        """Clear cache and refill it"""
        if socket is None:
            socket = lookup_socket(self._protocol)

        capi.nl_cache_refill(socket._sock, self._nl_cache)
        return self

    def resync(self, socket=None, cb=None, args=None):
        """Synchronize cache with content in kernel"""
        if socket is None:
            socket = lookup_socket(self._protocol)

        capi.nl_cache_resync(socket._sock, self._nl_cache, cb, args)

    def provide(self):
        """Provide this cache to others

        Caches which have been "provided" are made available
        to other users (of the same application context) which
        "require" it. F.e. a link cache is generally provided
        to allow others to translate interface indexes to
        link names
        """

        capi.nl_cache_mngt_provide(self._nl_cache)

    def unprovide(self):
        """Unprovide this cache

        No longer make the cache available to others. If the cache
        has been handed out already, that reference will still
        be valid.
        """
        capi.nl_cache_mngt_unprovide(self._nl_cache)


# Cache Manager (Work in Progress)
NL_AUTO_PROVIDE = 1


class CacheManager(object):
    def __init__(self, protocol, flags=None):

        self._sock = Socket()
        self._sock.connect(protocol)

        if not flags:
            flags = NL_AUTO_PROVIDE

        self._mngr = capi.cache_mngr_alloc(self._sock._sock, protocol, flags)

    def __del__(self):
        if self._sock:
            self._sock.disconnect()

        if self._mngr:
            capi.nl_cache_mngr_free(self._mngr)

    def add(self, name):
        capi.cache_mngr_add(self._mngr, name, None, None)


class AddressFamily(object):
    """Address family representation

    af = AddressFamily('inet6')
    # raises:
    #   - ValueError if family name is not known
    #   - TypeError if invalid type is specified for family

    print af        # => 'inet6' (string representation)
    print int(af)   # => 10 (numeric representation)
    print repr(af)  # => AddressFamily('inet6')
    """

    def __init__(self, family=socket.AF_UNSPEC):
        if isinstance(family, str):
            family = capi.nl_str2af(family)
            if family < 0:
                raise ValueError("Unknown family name")
        elif not isinstance(family, int):
            raise TypeError()

        self._family = family

    def __str__(self):
        return capi.nl_af2str(self._family, 32)[0]

    def __int__(self):
        return self._family

    def __repr__(self):
        return "AddressFamily({0!r})".format(str(self))


class AbstractAddress(object):
    """Abstract address object

    addr = AbstractAddress('127.0.0.1/8')
    print addr               # => '127.0.0.1/8'
    print addr.prefixlen     # => '8'
    print addr.family        # => 'inet'
    print len(addr)          # => '4' (32bit ipv4 address)

    a = AbstractAddress('10.0.0.1/24')
    b = AbstractAddress('10.0.0.2/24')
    print a == b             # => False


    """

    def __init__(self, addr):
        self._nl_addr = None

        if isinstance(addr, str):
            # returns None on success I guess
            # TO CORRECT
            addr = capi.addr_parse(addr, socket.AF_UNSPEC)
            if addr is None:
                raise ValueError("Invalid address format")
        elif addr:
            capi.nl_addr_get(addr)

        self._nl_addr = addr

    def __del__(self):
        if self._nl_addr:
            capi.nl_addr_put(self._nl_addr)

    def __cmp__(self, other):
        if isinstance(other, str):
            other = AbstractAddress(other)

        diff = self.prefixlen - other.prefixlen
        if diff == 0:
            diff = capi.nl_addr_cmp(self._nl_addr, other._nl_addr)

        return diff

    def contains(self, item):
        diff = int(self.family) - int(item.family)
        if diff:
            return False

        if item.prefixlen < self.prefixlen:
            return False

        diff = capi.nl_addr_cmp_prefix(self._nl_addr, item._nl_addr)
        return diff == 0

    def __nonzero__(self):
        if self._nl_addr:
            return not capi.nl_addr_iszero(self._nl_addr)
        else:
            return False

    def __len__(self):
        if self._nl_addr:
            return capi.nl_addr_get_len(self._nl_addr)
        else:
            return 0

    def __str__(self):
        if self._nl_addr:
            return capi.nl_addr2str(self._nl_addr, 64)[0]
        else:
            return "none"

    @property
    def shared(self):
        """True if address is shared (multiple users)"""
        if self._nl_addr:
            return capi.nl_addr_shared(self._nl_addr) != 0
        else:
            return False

    @property
    def prefixlen(self):
        """Length of prefix (number of bits)"""
        if self._nl_addr:
            return capi.nl_addr_get_prefixlen(self._nl_addr)
        else:
            return 0

    @prefixlen.setter
    def prefixlen(self, value):
        if not self._nl_addr:
            raise TypeError()

        capi.nl_addr_set_prefixlen(self._nl_addr, int(value))

    @property
    def family(self):
        """Address family"""
        f = 0
        if self._nl_addr:
            f = capi.nl_addr_get_family(self._nl_addr)

        return AddressFamily(f)

    @family.setter
    def family(self, value):
        if not self._nl_addr:
            raise TypeError()

        if not isinstance(value, AddressFamily):
            value = AddressFamily(value)

        capi.nl_addr_set_family(self._nl_addr, int(value))


# keyword:
#   type = { int | str }
#   immutable = { True | False }
#   fmt = func (formatting function)
#   title = string


def nlattr(**kwds):
    """netlink object attribute decorator

    decorator used to mark mutable and immutable properties
    of netlink objects. All properties marked as such are
    regarded to be accessable.

    @property
    @netlink.nlattr(type=int)
    def my_attr(self):
        return self._my_attr

    """

    def wrap_fn(func):
        func.formatinfo = kwds
        return func

    return wrap_fn
