"""
   Copyright (C) 2011-2015 Jiri Pirko <jiri@resnulli.us>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
"""

__author__ = """
jiri@resnulli.us (Jiri Pirko)
"""

import capi
import select
import struct

TEAM_ANY_CHANGE = capi.TEAM_ANY_CHANGE
TEAM_PORT_CHANGE = capi.TEAM_PORT_CHANGE
TEAM_OPTION_CHANGE = capi.TEAM_OPTION_CHANGE

class TeamError(Exception):
    pass

class TeamLibError(TeamError):
    def __init__(self, msg, err = 0):
        self._msg = msg
        self._err = err

    def __str__(self):
        msg = self._msg
        if self._err:
            msg += " Err=%d" % self._err
        return msg

class TeamUnknownOptionTypeError(TeamError):
    pass

class TeamNetDeviceIndexNameConverter(object):
    """
    Class does conversion between various network device identificators.
    Note both functions have DeviceID as input parameter.
    That can be either: interface index (int)
                        interface name (str)
                        TeamNetDevice (or inheritor) instance
    """
    def __init__(self, th):
        self._th = th

    def get_ifindex(self, dev_id):
        """
        Get interface index.
        """
        if isinstance(dev_id, int):
            return dev_id
        elif isinstance(dev_id, TeamNetDevice):
            return dev_id.ifindex
        elif isinstance(dev_id, str):
            return capi.team_ifname2ifindex(self._th, dev_id)
        raise TeamError("Cannot convert to interface index.")

    def dev_ifname(self, dev_id):
        """
        Get interface name.
        """
        if isinstance(dev_id, str):
            return dev_id
        elif isinstance(dev_id, TeamNetDevice):
            ifindex = dev_id.ifindex
        elif isinstance(dev_id, int):
            ifindex = dev_id
        else:
            raise TeamError("Cannot convert to interface name.")
        return capi.team_ifindex2ifname(self._th, ifindex, 32)

class TeamNetDevice(object):
    """
    Class for manipulating generic network device.
    """
    def __init__(self, th, ifindex = 0):
        self._th = th
        self._conv = TeamNetDeviceIndexNameConverter(th)
        self.ifindex = ifindex

    def __str__(self):
        return self.ifname

    @property
    def ifindex(self):
        return self._ifindex

    @ifindex.setter
    def ifindex(self, ifindex):
        self._ifindex = ifindex
        self.ifname = self._conv.dev_ifname(ifindex)

    def get_hwaddr(self):
        err, hwaddr = capi.team_hwaddr_get(self._th, self.ifindex, 6)
        if err:
            raise TeamLibError("Failed to get hardware address", err)
        return ":".join(map(lambda x: "%02X" % x, struct.unpack('BBBBBB', hwaddr)))

    def set_hwaddr(self, hwaddr_str):
        pack = struct.pack('BBBBBB', *map(lambda x : int(x, 16), hwaddr_str.split(":")))
        err = capi.team_hwaddr_set(self._th, self.ifindex, pack)
        if err:
            raise TeamLibError("Failed to set hardware address", err)

class TeamPort(TeamNetDevice):
    """
    Class stores port data and serves for port modification.
    """
    def update(self, lib_port):
        """
        Update option by give library structure.
        """
        self.speed = capi.team_get_port_speed(lib_port)
        self.duplex = capi.team_get_port_duplex(lib_port)
        self.changed = capi.team_is_port_changed(lib_port)
        self.linkup = capi.team_is_port_link_up(lib_port)
        self.removed = capi.team_is_port_removed(lib_port)

class TeamPortListIterator(object):
    """
    Iterator class for TeamPortList class for iterating over all listed ports.
    """
    def __init__(self, ports):
        self._ports = ports
        self._cursor = 0

    def __iter__(self):
        iter

    def next(self):
        """ Get next item in dict """
        if self._cursor == len(self._ports):
            raise StopIteration
        else:
            key = self._ports.keys()[self._cursor]
            self._cursor += 1
            return self._ports[key]

class TeamPortList(object):
    """
    Class contains list of ports present on team. Dictionary is used
    internaly since port interface index is unique.
    """
    def __init__(self, th):
        self._th = th
        self._conv = TeamNetDeviceIndexNameConverter(th)
        self._ports = {}
        self.update()

    def __len__(self):
        return len(self._ports)

    def __iter__(self):
        return TeamPortListIterator(self._ports)

    def get_port(self, port_dev_id):
        """
        Get port instance identified by DeviceID.
        """
        return self._ports[self._conv.get_ifindex(port_dev_id)]

    def update(self):
        """
        Fetch fresh data from library and adjust update dictionary and port
        instances by it.
        """
        lib_port_ifindex_list = []
        lib_port = capi.team_get_next_port(self._th, None)
        while lib_port:
            ifindex = capi.team_get_port_ifindex(lib_port)
            lib_port_ifindex_list.append(ifindex)
            try:
                port = self.get_port(ifindex)
            except KeyError:
                port = TeamPort(self._th, ifindex)
                self._ports[ifindex] = port
            port.update(lib_port)
            lib_port = capi.team_get_next_port(self._th, lib_port)

        for ifindex in self._ports.keys():
            if not ifindex in lib_port_ifindex_list:
                del self._ports[ifindex]

class TeamOption(object):
    """
    Class stores option data and serves for value modification.
    """
    def __init__(self, th, name):
        self._th = th
        self.name = name

    def _get_option_value(self, lib_option):
        opt_type = capi.team_get_option_type(lib_option)
        if opt_type == capi.TEAM_OPTION_TYPE_U32:
            return capi.team_get_option_value_u32(lib_option)
        elif opt_type == capi.TEAM_OPTION_TYPE_STRING:
            return capi.team_get_option_value_string(lib_option)
        else:
            raise TeamUnknownOptionTypeError()

    def update(self, lib_option):
        """
        Update option by give library structure.
        """
        self.value = self._get_option_value(lib_option)
        self.changed = capi.team_is_option_changed(lib_option)

    def set_value(self, value):
        """
        Set option value.
        """
        if isinstance(value, int):
            err = capi.team_set_option_value_by_name_u32(self._th, self.name,
                                                         value)
        elif isinstance(value, str):
            err = capi.team_set_option_value_by_name_string(self._th,
                                                            self.name,
                                                            value)
        else:
            raise TeamUnknownOptionTypeError()
        if err:
            raise TeamLibError("Failed to set option", err)

class TeamOptionListIterator(object):
    """
    Iterator class for TeamOptionList class for iterating over all options.
    """
    def __init__(self, options):
        self._options = options
        self._cursor = 0

    def __iter__(self):
        iter

    def next(self):
        """ Get next item in dict """
        if self._cursor == len(self._options):
            raise StopIteration
        else:
            key = self._options.keys()[self._cursor]
            self._cursor += 1
            return self._options[key]

class TeamOptionList(object):
    """
    Class contains list of options present on team. Dictionary is used
    internaly since option name is unique.
    """
    def __init__(self, th):
        self._th = th
        self._options = {}
        self.update()

    def __len__(self):
        return len(self._options)

    def __iter__(self):
        return TeamOptionListIterator(self._options)

    def get_option(self, opt_name):
        """
        Get option instance identified by name.
        """
        return self._options[opt_name]

    def update(self):
        """
        Fetch fresh data from library and adjust update dictionary and option
        instances by it.
        """
        lib_option_name_list = []
        lib_option = capi.team_get_next_option(self._th, None)
        while lib_option:
            opt_name = capi.team_get_option_name(lib_option)
            lib_option_name_list.append(opt_name)
            try:
                option = self.get_option(opt_name)
            except KeyError:
                option = TeamOption(self._th, opt_name)
                self._options[opt_name] = option
            try:
                option.update(lib_option)
            except TeamUnknownOptionTypeError:
                del self._options[opt_name]
            lib_option = capi.team_get_next_option(self._th, lib_option)

        for opt_name in self._options.keys():
            if not opt_name in lib_option_name_list:
                del self._options[opt_name]

class TeamChangeHandler(object):
    def __init__(self, func, func_priv, type_mask):
        self._func = func
        self._func_priv = func_priv
        self._type_mask = type_mask

    def call(self, curr_type_mask):
        if self._type_mask & curr_type_mask:
            return self._func(self._func_priv)
        else:
            return 0

class TeamChangeHandlerList(object):
    def __init__(self):
        self._list = []

    def add(self, handler):
        if handler in self._list:
            raise TeamError("Failed to register change handler. Handler is already registered.")
        self._list.append(handler)

    def remove(self, handler):
        if not handler in self._list:
            raise TeamError("Failed to unregister change handler. Handler is not registered.")
        self._list.remove(handler)

    def call(self, type_mask):
        for handler in self._list:
            ret = handler.call(type_mask)
            if ret != 0:
                return ret


class Team(TeamNetDevice):
    """
    Class representing one team device instance.
    Paramaters passed to constructor allows:
        create == True ... Create new team device if it does not already exist.
        recreate == True ... Same as create but in case device exists already,
                             it's removed first.
        destroy == True ... Remove team device in close function.
    """
    def __init__(self, teamdev, create = False, recreate = False, destroy = False):
        th = capi.team_alloc()
        if not th:
            raise TeamLibError("Failed to allocate team handle.")

        super(Team, self).__init__(th)

        if isinstance(teamdev, str):
            err = 0
            if recreate:
                err = capi.team_recreate(th, teamdev)
            elif create:
                err = capi.team_create(th, teamdev)
            if err:
                raise TeamLibError("Failed to create team.", err)

        ifindex = self._conv.get_ifindex(teamdev) if teamdev else 0
        err = capi.team_init(th, ifindex)
        if err:
            raise TeamLibError("Failed to init team.", err)

        self.ifindex = ifindex
        self._destroy = destroy
        self._change_handler_list = TeamChangeHandlerList()
        self._port_list = TeamPortList(th)
        self._option_list = TeamOptionList(th)

        self._change_handler = capi.team_change_handler(self._change_handler_func,
                                                        TEAM_ANY_CHANGE)
        capi.py_team_change_handler_register(self._th, self._change_handler, None)


    def close(self):
        """
        Do class cleanup
        """
        if self._destroy:
            err = capi.team_destroy(self._th)
            if err:
                raise TeamLibError("Failed to destroy team.", err)

        capi.py_team_change_handler_unregister(self._th,
                                               self._change_handler, None)
        capi.team_free(self._th)

    def kill_loop(self):
        self._kill_loop = True

    def loop_forever(self):
        self._kill_loop = False
        fd = capi.team_get_event_fd(self._th)
        while True:
            try:
                ret = select.select([fd], [], [])
                if fd in ret[0]:
                    capi.team_handle_events(self._th)
            except KeyboardInterrupt:
                return
            except select.error as e:
                if e[0] == 4:
                    if self._kill_loop:
                        return
            except:
                raise

    def check_events(self):
        capi.team_check_events(self._th)

    def _change_handler_func(self, func_priv, type_mask):
        if type_mask & TEAM_PORT_CHANGE:
            self._port_list.update()
        if type_mask & TEAM_OPTION_CHANGE:
            self._option_list.update()
        self._change_handler_list.call(type_mask)

    def change_handler_register(self, change_handler):
        self._change_handler_list.add(change_handler)

    def change_handler_unregister(self, change_handler):
        self._change_handler_list.remove(change_handler)

    def get_mode_name(self):
        err, name = capi.team_get_mode_name(self._th)
        if err:
            raise TeamLibError("Failed to get mode name.", err)
        return name

    def set_mode_name(self, name):
        err = capi.team_set_mode_name(self._th, name)
        if err:
            raise TeamLibError("Failed to set mode name.", err)

    def get_active_port(self):
        err, port_ifindex = capi.team_get_active_port(self._th)
        if err:
            raise TeamLibError("Failed to get active port.", err)
        return self.get_port(port_ifindex)

    def set_active_port(self, dev_port_id):
        err = capi.team_set_active_port(self._th,
                                        self._conv.get_ifindex(dev_port_id))
        if err:
            raise TeamLibError("Failed to set active port.", err)

    def port_list(self):
        return self._port_list

    def get_port(self, port_dev_id):
        return self._port_list.get_port(port_dev_id)

    def port_add(self, port_dev_id):
        err = capi.team_port_add(self._th,
                                 self._conv.get_ifindex(port_dev_id))
        if err:
            raise TeamLibError("Failed to add port.", err)

    def port_remove(self, port_dev_id):
        err = capi.team_port_remove(self._th,
                                    self._conv.get_ifindex(port_dev_id))
        if err:
            raise TeamLibError("Failed to remove port.", err)

    def option_list(self):
        return self._option_list

    def get_option(self, opt_name):
        return self._option_list.get_option(opt_name)
