# Copyright (C) 2014 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.

import collections

from gi.repository import Gtk

import virtinst
from virtinst import log

from ..lib import uiutil
from ..baseclass import vmmGObjectUI


NetDev = collections.namedtuple('Netdev', ['name', 'is_bridge', 'slave_names'])


class vmmNetworkList(vmmGObjectUI):
    __gsignals__ = {
        "changed": (vmmGObjectUI.RUN_FIRST, None, []),
        "changed-vport": (vmmGObjectUI.RUN_FIRST, None, [])
    }

    def __init__(self, conn, builder, topwin):
        vmmGObjectUI.__init__(self, "netlist.ui",
                              None, builder=builder, topwin=topwin)
        self.conn = conn

        self.builder.connect_signals({
            "on_net_source_changed": self._on_net_source_changed,
            "on_net_source_mode_changed": self._emit_changed,
            "on_net_portgroup_changed": self._emit_changed,
            "on_net_bridge_name_changed": self._emit_changed,

            "on_vport_type_changed": self._emit_vport_changed,
            "on_vport_managerid_changed": self._emit_vport_changed,
            "on_vport_typeid_changed": self._emit_vport_changed,
            "on_vport_typeidversion_changed": self._emit_vport_changed,
            "on_vport_instanceid_changed": self._emit_vport_changed,
        })

        self._init_ui()
        self.top_label = self.widget("net-source-label")
        self.top_box = self.widget("net-source-box")
        self.top_vport = self.widget("vport-expander")

    def _cleanup(self):
        self.conn.disconnect_by_obj(self)
        self.conn = None

        self.top_label.destroy()
        self.top_box.destroy()
        self.top_vport.destroy()


    ##########################
    # Initialization methods #
    ##########################

    def _init_ui(self):
        # [ network type, source name, label, sensitive?, net is active,
        #   manual bridge, net instance]
        model = Gtk.ListStore(str, str, str, bool, bool, bool, object)
        combo = self.widget("net-source")
        combo.set_model(model)

        text = Gtk.CellRendererText()
        combo.pack_start(text, True)
        combo.add_attribute(text, 'text', 2)
        combo.add_attribute(text, 'sensitive', 3)

        combo = self.widget("net-source-mode")
        # [xml value, label]
        model = Gtk.ListStore(str, str)
        combo.set_model(model)
        uiutil.init_combo_text_column(combo, 1)

        model.append(["bridge", _("Bridge")])
        model.append(["vepa", "VEPA"])
        model.append(["private", _("Private")])
        model.append(["passthrough", _("Passthrough")])
        combo.set_active(0)

        combo = self.widget("net-portgroup")
        # [xml value, label]
        model = Gtk.ListStore(str, str)
        combo.set_model(model)
        uiutil.init_combo_text_column(combo, 1)

        self.conn.connect("net-added", self._repopulate_network_list)
        self.conn.connect("net-removed", self._repopulate_network_list)
        self.conn.connect("interface-added", self._repopulate_network_list)
        self.conn.connect("interface-removed", self._repopulate_network_list)

    def _pretty_network_desc(self, nettype, source=None, netobj=None):
        if nettype == virtinst.DeviceInterface.TYPE_USER:
            return _("Usermode networking")

        extra = None
        if nettype == virtinst.DeviceInterface.TYPE_BRIDGE:
            ret = _("Bridge")
        elif nettype == virtinst.DeviceInterface.TYPE_VIRTUAL:
            ret = _("Virtual network")
            if netobj:
                extra = ": %s" % netobj.pretty_forward_mode()
        else:
            ret = nettype.capitalize()

        if source:
            ret += " '%s'" % source
        if extra:
            ret += " %s" % extra

        return ret

    def _build_source_row(self, nettype, source_name,
            label, is_sensitive, is_running, manual_bridge=False, key=None):
        return [nettype, source_name, label,
                is_sensitive, is_running, manual_bridge,
                key]

    def _find_virtual_networks(self):
        rows = []
        vnet_bridges = []
        default_label = None

        for net in self.conn.list_nets():
            nettype = virtinst.DeviceInterface.TYPE_VIRTUAL

            label = self._pretty_network_desc(nettype, net.get_name(), net)
            if not net.is_active():
                label += " (%s)" % _("Inactive")

            if net.get_xmlobj().virtualport_type == "openvswitch":
                label += " (OpenVSwitch)"

            if net.get_name() == "default":
                default_label = label

            rows.append(self._build_source_row(
                nettype, net.get_name(), label, True,
                net.is_active(), key=net.get_connkey()))

            # Build a list of vnet bridges, so we know not to list them
            # in the physical interface list
            vnet_bridge = net.get_bridge_device()
            if vnet_bridge:
                vnet_bridges.append(vnet_bridge)

        if not rows:
            label = _("No virtual networks available")
            rows.append(self._build_source_row(
                None, None, label, False, False))

        return rows, vnet_bridges, default_label

    def _find_physical_devices(self, vnet_bridges):
        rows = []
        can_default = False
        default_label = None
        skip_ifaces = ["lo"]

        vnet_taps = []
        for vm in self.conn.list_vms():
            for nic in vm.get_interface_devices_norefresh():
                if nic.target_dev and nic.target_dev not in vnet_taps:
                    vnet_taps.append(nic.target_dev)

        netdevs = {}
        for iface in self.conn.list_interfaces():
            name = iface.get_name()
            netdevs[name] = NetDev(name, iface.is_bridge(),
                                   iface.get_interface_names())
        for nodedev in self.conn.filter_nodedevs("net"):
            if nodedev.xmlobj.interface not in netdevs:
                netdev = NetDev(nodedev.xmlobj.interface, False, [])
                netdevs[nodedev.xmlobj.interface] = netdev

        # For every bridge used by a virtual network, and any slaves of
        # those devices, don't list them.
        for vnet_bridge in vnet_bridges:
            slave_names = netdevs.pop(vnet_bridge,
                                      NetDev(None, None, [])).slave_names
            for slave in slave_names:
                netdevs.pop(slave, None)

        for name, is_bridge, slave_names in list(netdevs.values()):
            if ((name in vnet_taps) or
                (name in [v + "-nic" for v in vnet_bridges]) or
                (name in skip_ifaces)):
                # Don't list this, as it is basically duplicating
                # virtual net info
                continue

            sensitive = True
            source_name = name

            label = _("Host device %s") % (name)
            if is_bridge:
                nettype = virtinst.DeviceInterface.TYPE_BRIDGE
                if slave_names:
                    extra = (_("Host device %s") % slave_names[0])
                    can_default = True
                else:
                    extra = _("Empty bridge")
                label = _("Bridge %s: %s") % (name, extra)

            elif self.conn.is_qemu() or self.conn.is_test():
                nettype = virtinst.DeviceInterface.TYPE_DIRECT
                label += (": %s" % _("macvtap"))

            else:
                nettype = None
                sensitive = False
                source_name = None
                label += (": %s" % _("Not bridged"))

            if can_default and not default_label:
                default_label = label

            rows.append(self._build_source_row(
                nettype, source_name, label, sensitive, True,
                key=name))

        return rows, default_label

    def _populate_network_model(self, model):
        model.clear()

        def _add_manual_bridge_row():
            manual_row = self._build_source_row(
                None, None, _("Specify shared device name"),
                True, False, manual_bridge=True)
            model.append(manual_row)

        if self.conn.is_qemu_session():
            nettype = virtinst.DeviceInterface.TYPE_USER
            r = self._build_source_row(
                nettype, None, self._pretty_network_desc(nettype), True, True)
            model.append(r)

            _add_manual_bridge_row()
            return

        (vnets, vnet_bridges, default_net) = self._find_virtual_networks()
        (iface_rows, default_bridge) = self._find_physical_devices(
            vnet_bridges)

        # Sorting is:
        # 1) Bridges
        # 2) Virtual networks
        # 3) direct/macvtap
        # 4) Disabled list entries
        # Each category sorted alphabetically
        bridges = [row for row in iface_rows if row[0] == "bridge"]
        direct = [row for row in iface_rows if row[0] == "direct"]
        disabled = [row for row in iface_rows if row[0] is None]

        for rows in [bridges, vnets, direct, disabled]:
            rows.sort(key=lambda r: r[2])
            for row in rows:
                model.append(row)

        # If there is a bridge device, default to that
        # If not, use 'default' network
        # If not present, use first list entry
        # If list empty, use no network devices
        label = default_bridge or default_net

        default = 0
        if not len(model):
            row = self._build_source_row(
                None, None, _("No networking"), True, False)
            model.insert(0, row)
            default = 0
        elif label:
            default = [idx for idx, model_label in enumerate(model) if
                       model_label[2] == label][0]

        _add_manual_bridge_row()
        return default

    def _check_network_is_running(self, net):
        # Make sure VirtualNetwork is running
        if not net.type == virtinst.DeviceInterface.TYPE_VIRTUAL:
            return
        devname = net.source

        netobj = None
        if net.type == virtinst.DeviceInterface.TYPE_VIRTUAL:
            for n in self.conn.list_nets():
                if n.get_name() == devname:
                    netobj = n
                    break

        if not netobj or netobj.is_active():
            return

        res = self.err.yes_no(_("Virtual Network is not active."),
            _("Virtual Network '%s' is not active. "
              "Would you like to start the network "
              "now?") % devname)
        if not res:
            return

        # Try to start the network
        try:
            netobj.start()
            log.debug("Started network '%s'", devname)
        except Exception as e:
            return self.err.show_err(_("Could not start virtual network "
                                  "'%s': %s") % (devname, str(e)))


    ###############
    # Public APIs #
    ###############

    def get_network_row(self):
        return uiutil.get_list_selected_row(self.widget("net-source"))

    def get_network_selection(self):
        bridge_entry = self.widget("net-bridge-name")
        row = self.get_network_row()
        if not row:
            return None, None, None, None

        net_type = row[0]
        net_src = row[1]
        net_check_bridge = row[5]

        if net_check_bridge and bridge_entry:
            net_type = virtinst.DeviceInterface.TYPE_BRIDGE
            net_src = bridge_entry.get_text() or None

        mode = None
        if self.widget("net-source-mode").is_visible():
            mode = uiutil.get_list_selection(self.widget("net-source-mode"))

        portgroup = None
        if self.widget("net-portgroup").is_visible():
            portgroup = uiutil.get_list_selection(self.widget("net-portgroup"))

        return net_type, net_src, mode, portgroup or None

    def get_vport(self):
        vport_type = self.widget("vport-type").get_text()
        vport_managerid = self.widget("vport-managerid").get_text()
        vport_typeid = self.widget("vport-typeid").get_text()
        vport_idver = self.widget("vport-typeidversion").get_text()
        vport_instid = self.widget("vport-instanceid").get_text()

        return (vport_type, vport_managerid, vport_typeid,
         vport_idver, vport_instid)

    def build_device(self, macaddr, model=None):
        nettype, devname, mode, portgroup = self.get_network_selection()

        net = virtinst.DeviceInterface(self.conn.get_backend())
        net.type = nettype
        net.source = devname
        net.macaddr = macaddr
        net.model = model
        net.source_mode = mode
        net.portgroup = portgroup
        if net.model == "spapr-vlan":
            net.address.set_addrstr("spapr-vio")

        if net.type == "direct":
            (vport_type, vport_managerid, vport_typeid,
             vport_idver, vport_instid) = self.get_vport()

            net.virtualport.type = vport_type or None
            net.virtualport.managerid = vport_managerid or None
            net.virtualport.typeid = vport_typeid or None
            net.virtualport.typeidversion = vport_idver or None
            net.virtualport.instanceid = vport_instid or None

        return net

    def validate_device(self, net):
        self._check_network_is_running(net)
        net.validate()

    def reset_state(self):
        self._repopulate_network_list()

        net_err = None
        if (not self.conn.is_nodedev_capable() or
            not self.conn.is_interface_capable()):
            net_err = _("Libvirt version does not support "
                        "physical interface listing.")

        net_warn = self.widget("net-source-warn")
        net_warn.set_visible(bool(net_err))
        net_warn.set_tooltip_text(net_err or "")

        self.widget("net-bridge-name").set_text("")
        self.widget("net-source-mode").set_active(0)
        self.widget("net-portgroup").get_child().set_text("")

        self.widget("vport-type").set_text("")
        self.widget("vport-managerid").set_text("")
        self.widget("vport-typeid").set_text("")
        self.widget("vport-typeidversion").set_text("")
        self.widget("vport-instanceid").set_text("")

    def set_dev(self, net):
        self.reset_state()

        nettype = net.type
        source = net.source
        if net.network:
            # If using type=network with a forward mode=bridge network,
            # on domain startup the runtime XML will be changed to
            # type=bridge and both source/@bridge and source/@network will
            # be filled in. For our purposes, treat this as a type=network
            source = net.network
            nettype = "network"

        source_mode = net.source_mode
        is_direct = (net.type == "direct")

        uiutil.set_list_selection(self.widget("net-source-mode"), source_mode)

        # Virtualport config
        self.widget("vport-expander").set_visible(is_direct)

        vport = net.virtualport
        self.widget("vport-type").set_text(vport.type or "")
        self.widget("vport-managerid").set_text(str(vport.managerid or ""))
        self.widget("vport-typeid").set_text(str(vport.typeid or ""))
        self.widget("vport-typeidversion").set_text(
            str(vport.typeidversion or ""))
        self.widget("vport-instanceid").set_text(vport.instanceid or "")

        # Find the matching row in the net list
        combo = self.widget("net-source")
        rowiter = None
        for row in combo.get_model():
            if row[0] == nettype and row[1] == source:
                rowiter = row.iter
                break
        if not rowiter:
            if nettype == "bridge":
                rowiter = combo.get_model()[-1].iter
                self.widget("net-bridge-name").set_text(source)
        if not rowiter:
            desc = self._pretty_network_desc(nettype, source)
            combo.get_model().insert(0,
                self._build_source_row(nettype, source, desc, True, True))
            rowiter = combo.get_model()[0].iter

        combo.set_active_iter(rowiter)
        combo.emit("changed")

        if net.portgroup:
            uiutil.set_list_selection(self.widget("net-portgroup"), net.portgroup)


    #############
    # Listeners #
    #############

    def _emit_changed(self, *args, **kwargs):
        ignore1 = args
        ignore2 = kwargs
        self.emit("changed")

    def _emit_vport_changed(self, *args, **kwargs):
        ignore1 = args
        ignore2 = kwargs
        self.emit("changed-vport")

    def _repopulate_network_list(self, *args, **kwargs):
        ignore1 = args
        ignore2 = kwargs

        netlist = self.widget("net-source")
        current_label = uiutil.get_list_selection(netlist, column=2)

        model = netlist.get_model()
        if not model:
            return

        try:
            if model:
                netlist.set_model(None)
                default_idx = self._populate_network_model(model)
        finally:
            netlist.set_model(model)

        for row in netlist.get_model():
            if current_label and row[2] == current_label:
                netlist.set_active_iter(row.iter)
                return

        if default_idx is None:
            default_idx = 0
        netlist.set_active(default_idx)


    def _populate_portgroups(self, portgroups):
        combo = self.widget("net-portgroup")
        model = combo.get_model()
        model.clear()

        default = None
        for p in portgroups:
            model.append([p.name, p.name])
            if p.default:
                default = p.name

        uiutil.set_list_selection(combo, default)

    def _on_net_source_changed(self, src):
        ignore = src
        self._emit_changed()
        row = self.get_network_row()
        if not row:
            return

        is_openvswitch = row[2].endswith("(OpenVSwitch)")
        is_direct = (row[0] == virtinst.DeviceInterface.TYPE_DIRECT)
        self.widget("vport-expander").set_visible(is_direct or is_openvswitch)
        uiutil.set_grid_row_visible(self.widget("net-source-mode"), is_direct)
        uiutil.set_grid_row_visible(
            self.widget("net-macvtap-warn-box"), is_direct)
        if is_direct and self.widget("net-source-mode").get_active() == -1:
            self.widget("net-source-mode").set_active(0)

        show_bridge = row[5]
        uiutil.set_grid_row_visible(
            self.widget("net-bridge-name"), show_bridge)

        portgroups = []
        connkey = row[6]
        if connkey and row[0] == virtinst.DeviceInterface.TYPE_VIRTUAL:
            portgroups = self.conn.get_net(connkey).get_xmlobj().portgroups

        uiutil.set_grid_row_visible(
            self.widget("net-portgroup"), bool(portgroups))
        self._populate_portgroups(portgroups)
