#
# Copyright (c) 2011 Thomas Graf <tgraf@suug.ch>
#

"""Module providing access to network addresses
"""

from __future__ import absolute_import


__version__ = "1.0"
__all__ = ["AddressCache", "Address"]

import datetime
from .. import core as netlink
from . import capi as capi
from . import link as Link
from .. import util as util


class AddressCache(netlink.Cache):
    """Cache containing network addresses"""

    def __init__(self, cache=None):
        if not cache:
            cache = self._alloc_cache_name("route/addr")

        self._protocol = netlink.NETLINK_ROUTE
        self._nl_cache = cache

    def __getitem__(self, key):
        # Using ifindex=0 here implies that the local address itself
        # is unique, otherwise the first occurence is returned.
        return self.lookup(0, key)

    def lookup(self, ifindex, local):
        if type(local) is str:
            local = netlink.AbstractAddress(local)

        addr = capi.rtnl_addr_get(self._nl_cache, ifindex, local._nl_addr)
        if addr is None:
            raise KeyError()

        return Address._from_capi(addr)

    @staticmethod
    def _new_object(obj):
        return Address(obj)

    @staticmethod
    def _new_cache(cache):
        return AddressCache(cache=cache)


class Address(netlink.Object):
    """Network address"""

    def __init__(self, obj=None):
        netlink.Object.__init__(self, "route/addr", "address", obj)
        self._rtnl_addr = self._obj2type(self._nl_object)

    @classmethod
    def _from_capi(cls, obj):
        return cls(capi.addr2obj(obj))

    @staticmethod
    def _obj2type(obj):
        return capi.obj2addr(obj)

    def __cmp__(self, other):
        # sort by:
        #    1. network link
        #    2. address family
        #    3. local address (including prefixlen)
        diff = self.ifindex - other.ifindex

        if diff == 0:
            diff = self.family - other.family
            if diff == 0:
                diff = capi.nl_addr_cmp(self.local, other.local)

        return diff

    @staticmethod
    def _new_instance(obj):
        return Address(obj)

    @property
    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
    def ifindex(self):
        """interface index"""
        return capi.rtnl_addr_get_ifindex(self._rtnl_addr)

    @ifindex.setter
    def ifindex(self, value):
        link = Link.resolve(value)
        if not link:
            raise ValueError()

        self.link = link

    @property
    @netlink.nlattr(type=str, fmt=util.string)
    def link(self):
        link = capi.rtnl_addr_get_link(self._rtnl_addr)
        if not link:
            return None

        return Link.Link.from_capi(link)

    @link.setter
    def link(self, value):
        if type(value) is str:
            try:
                value = Link.resolve(value)
            except KeyError:
                raise ValueError()

        capi.rtnl_addr_set_link(self._rtnl_addr, value._rtnl_link)

        # ifindex is immutable but we assume that if _orig does not
        # have an ifindex specified, it was meant to be given here
        if capi.rtnl_addr_get_ifindex(self._orig) == 0:
            capi.rtnl_addr_set_ifindex(self._orig, value.ifindex)

    @property
    @netlink.nlattr(type=str, fmt=util.string)
    def label(self):
        """address label"""
        return capi.rtnl_addr_get_label(self._rtnl_addr)

    @label.setter
    def label(self, value):
        capi.rtnl_addr_set_label(self._rtnl_addr, value)

    @property
    @netlink.nlattr(type=str, fmt=util.string)
    def flags(self):
        """Flags

        Setting this property will *Not* reset flags to value you supply in

        Examples:
        addr.flags = '+xxx' # add xxx flag
        addr.flags = 'xxx'  # exactly the same
        addr.flags = '-xxx' # remove xxx flag
        addr.flags = [ '+xxx', '-yyy' ] # list operation
        """
        flags = capi.rtnl_addr_get_flags(self._rtnl_addr)
        return capi.rtnl_addr_flags2str(flags, 256)[0].split(",")

    def _set_flag(self, flag):
        if flag.startswith("-"):
            i = capi.rtnl_addr_str2flags(flag[1:])
            capi.rtnl_addr_unset_flags(self._rtnl_addr, i)
        elif flag.startswith("+"):
            i = capi.rtnl_addr_str2flags(flag[1:])
            capi.rtnl_addr_set_flags(self._rtnl_addr, i)
        else:
            i = capi.rtnl_addr_str2flags(flag)
            capi.rtnl_addr_set_flags(self._rtnl_addr, i)

    @flags.setter
    def flags(self, value):
        if type(value) is list:
            for flag in value:
                self._set_flag(flag)
        else:
            self._set_flag(value)

    @property
    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
    def family(self):
        """Address family"""
        fam = capi.rtnl_addr_get_family(self._rtnl_addr)
        return netlink.AddressFamily(fam)

    @family.setter
    def family(self, value):
        if not isinstance(value, netlink.AddressFamily):
            value = netlink.AddressFamily(value)

        capi.rtnl_addr_set_family(self._rtnl_addr, int(value))

    @property
    @netlink.nlattr(type=int, fmt=util.num)
    def scope(self):
        """Address scope"""
        scope = capi.rtnl_addr_get_scope(self._rtnl_addr)
        return capi.rtnl_scope2str(scope, 32)[0]

    @scope.setter
    def scope(self, value):
        if type(value) is str:
            value = capi.rtnl_str2scope(value)
        capi.rtnl_addr_set_scope(self._rtnl_addr, value)

    @property
    @netlink.nlattr(type=str, immutable=True, fmt=util.addr)
    def local(self):
        """Local address"""
        a = capi.rtnl_addr_get_local(self._rtnl_addr)
        return netlink.AbstractAddress(a)

    @local.setter
    def local(self, value):
        a = netlink.AbstractAddress(value)
        capi.rtnl_addr_set_local(self._rtnl_addr, a._nl_addr)

        # local is immutable but we assume that if _orig does not
        # have a local address specified, it was meant to be given here
        if capi.rtnl_addr_get_local(self._orig) is None:
            capi.rtnl_addr_set_local(self._orig, a._nl_addr)

    @property
    @netlink.nlattr(type=str, fmt=util.addr)
    def peer(self):
        """Peer address"""
        a = capi.rtnl_addr_get_peer(self._rtnl_addr)
        return netlink.AbstractAddress(a)

    @peer.setter
    def peer(self, value):
        a = netlink.AbstractAddress(value)
        capi.rtnl_addr_set_peer(self._rtnl_addr, a._nl_addr)

    @property
    @netlink.nlattr(type=str, fmt=util.addr)
    def broadcast(self):
        """Broadcast address"""
        a = capi.rtnl_addr_get_broadcast(self._rtnl_addr)
        return netlink.AbstractAddress(a)

    @broadcast.setter
    def broadcast(self, value):
        a = netlink.AbstractAddress(value)
        capi.rtnl_addr_set_broadcast(self._rtnl_addr, a._nl_addr)

    @property
    @netlink.nlattr(type=str, fmt=util.addr)
    def multicast(self):
        """multicast address"""
        a = capi.rtnl_addr_get_multicast(self._rtnl_addr)
        return netlink.AbstractAddress(a)

    @multicast.setter
    def multicast(self, value):
        try:
            a = netlink.AbstractAddress(value)
        except ValueError as err:
            raise AttributeError("multicast", err)

        capi.rtnl_addr_set_multicast(self._rtnl_addr, a._nl_addr)

    @property
    @netlink.nlattr(type=str, fmt=util.addr)
    def anycast(self):
        """anycast address"""
        a = capi.rtnl_addr_get_anycast(self._rtnl_addr)
        return netlink.AbstractAddress(a)

    @anycast.setter
    def anycast(self, value):
        a = netlink.AbstractAddress(value)
        capi.rtnl_addr_set_anycast(self._rtnl_addr, a._nl_addr)

    @property
    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
    def valid_lifetime(self):
        """Valid lifetime"""
        msecs = capi.rtnl_addr_get_valid_lifetime(self._rtnl_addr)
        if msecs == 0xFFFFFFFF:
            return None
        else:
            return datetime.timedelta(seconds=msecs)

    @valid_lifetime.setter
    def valid_lifetime(self, value):
        capi.rtnl_addr_set_valid_lifetime(self._rtnl_addr, int(value))

    @property
    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
    def preferred_lifetime(self):
        """Preferred lifetime"""
        msecs = capi.rtnl_addr_get_preferred_lifetime(self._rtnl_addr)
        if msecs == 0xFFFFFFFF:
            return None
        else:
            return datetime.timedelta(seconds=msecs)

    @preferred_lifetime.setter
    def preferred_lifetime(self, value):
        capi.rtnl_addr_set_preferred_lifetime(self._rtnl_addr, int(value))

    @property
    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
    def create_time(self):
        """Creation time"""
        hsec = capi.rtnl_addr_get_create_time(self._rtnl_addr)
        return datetime.timedelta(milliseconds=10 * hsec)

    @property
    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
    def last_update(self):
        """Last update"""
        hsec = capi.rtnl_addr_get_last_update_time(self._rtnl_addr)
        return datetime.timedelta(milliseconds=10 * hsec)

    def add(self, socket=None, flags=None):
        if not socket:
            socket = netlink.lookup_socket(netlink.NETLINK_ROUTE)

        if not flags:
            flags = netlink.NLM_F_CREATE

        ret = capi.rtnl_addr_add(socket._sock, self._rtnl_addr, flags)
        if ret < 0:
            raise netlink.KernelError(ret)

    def delete(self, socket, flags=0):
        """Attempt to delete this address in the kernel"""
        ret = capi.rtnl_addr_delete(socket._sock, self._rtnl_addr, flags)
        if ret < 0:
            raise netlink.KernelError(ret)

    ###################################################################
    # private properties
    #
    # Used for formatting output. USE AT OWN RISK
    @property
    def _flags(self):
        return ",".join(self.flags)

    def format(self, details=False, stats=False, nodev=False, indent=""):
        """Return address as formatted text"""
        fmt = util.MyFormatter(self, indent)

        buf = fmt.format("{a|local!b}")

        if not nodev:
            buf += fmt.format(" {a|ifindex}")

        buf += fmt.format(" {a|scope}")

        if self.label:
            buf += fmt.format(' "{a|label}"')

        buf += fmt.format(" <{a|_flags}>")

        if details:
            buf += fmt.nl("\t{t|broadcast} {t|multicast}") + fmt.nl(
                "\t{t|peer} {t|anycast}"
            )

            if self.valid_lifetime:
                buf += fmt.nl("\t{s|valid-lifetime!k} " "{a|valid_lifetime}")

            if self.preferred_lifetime:
                buf += fmt.nl("\t{s|preferred-lifetime!k} " "{a|preferred_lifetime}")

        if stats and (self.create_time or self.last_update):
            buf += self.nl(
                "\t{s|created!k} {a|create_time}" " {s|last-updated!k} {a|last_update}"
            )

        return buf
