#
# Copyright (C) 2009, 2013 Red Hat, Inc.
# Copyright (C) 2009 Cole Robinson <crobinso@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301 USA.
#

import traceback
import logging
import threading

# pylint: disable=E0611
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Gtk
# pylint: enable=E0611

import libvirt
from virtinst import uriutil

from virtManager import util
from virtManager.baseclass import vmmGObjectUI
from virtManager.asyncjob import vmmAsyncJob
from virtManager.domain import vmmDomain


def uri_join(uri_tuple):
    scheme, user, host, path, query, fragment = uri_tuple

    user = (user and (user + "@") or "")
    host = host or ""
    path = path or "/"
    query = (query and ("?" + query) or "")
    fragment = (fragment and ("#" + fragment) or "")

    return "%s://%s%s%s%s%s" % (scheme, user, host, path, fragment, query)


class vmmMigrateDialog(vmmGObjectUI):
    def __init__(self, vm, engine):
        vmmGObjectUI.__init__(self, "vmm-migrate.ui", "vmm-migrate")
        self.vm = vm
        self.conn = vm.conn
        self.engine = engine

        self.destconn_rows = []

        self.builder.connect_signals({
            "on_vmm_migrate_delete_event" : self.close,

            "on_migrate_cancel_clicked" : self.close,
            "on_migrate_finish_clicked" : self.finish,

            "on_migrate_dest_changed" : self.destconn_changed,
            "on_migrate_set_rate_toggled" : self.toggle_set_rate,
            "on_migrate_set_interface_toggled" : self.toggle_set_interface,
            "on_migrate_set_port_toggled" : self.toggle_set_port,
            "on_migrate_set_maxdowntime_toggled" : self.toggle_set_maxdowntime,
        })
        self.bind_escape_key_close()

        blue = Gdk.color_parse("#0072A8")
        self.widget("migrate-header").modify_bg(Gtk.StateType.NORMAL,
                                                           blue)
        image = Gtk.Image.new_from_icon_name("vm_clone_wizard",
                                             Gtk.IconSize.DIALOG)
        image.show()
        self.widget("migrate-vm-icon-box").pack_end(image, False, False, False)

        self.init_state()

    def show(self, parent):
        logging.debug("Showing migrate wizard")
        self.reset_state()
        self.topwin.resize(1, 1)
        self.topwin.set_transient_for(parent)
        self.topwin.present()

    def close(self, ignore1=None, ignore2=None):
        logging.debug("Closing migrate wizard")
        self.topwin.hide()
        # If we only do this at show time, operation takes too long and
        # user actually sees the expander close.
        self.widget("migrate-advanced-expander").set_expanded(False)
        return 1

    def _cleanup(self):
        self.vm = None
        self.conn = None
        self.engine = None
        self.destconn_rows = None

        # Not sure why we need to do this manually, but it matters
        self.widget("migrate-dest").get_model().clear()

    def init_state(self):
        # [hostname, conn, can_migrate, tooltip]
        dest_model = Gtk.ListStore(str, object, bool, str)
        dest_combo = self.widget("migrate-dest")
        dest_combo.set_model(dest_model)
        text = Gtk.CellRendererText()
        dest_combo.pack_start(text, True)
        dest_combo.add_attribute(text, 'text', 0)
        dest_combo.add_attribute(text, 'sensitive', 2)
        dest_model.set_sort_column_id(0, Gtk.SortType.ASCENDING)

        # Hook up signals to get connection listing
        self.engine.connect("conn-added", self.dest_add_conn)
        self.engine.connect("conn-removed", self.dest_remove_conn)
        self.destconn_changed(dest_combo)

    def reset_state(self):
        title_str = ("<span size='large' color='white'>%s '%s'</span>" %
                     (_("Migrate"), util.xml_escape(self.vm.get_name())))
        self.widget("migrate-main-label").set_markup(title_str)

        self.widget("migrate-cancel").grab_focus()

        name = self.vm.get_name()
        srchost = self.conn.get_hostname()

        self.widget("migrate-label-name").set_text(name)
        self.widget("migrate-label-src").set_text(srchost)

        self.widget("migrate-set-interface").set_active(False)
        self.widget("migrate-set-rate").set_active(False)
        self.widget("migrate-set-port").set_active(False)
        self.widget("migrate-set-maxdowntime").set_active(False)
        self.widget("migrate-max-downtime").set_value(30)

        running = self.vm.is_active()
        self.widget("migrate-offline").set_active(not running)
        self.widget("migrate-offline").set_sensitive(running)

        self.widget("migrate-rate").set_value(0)
        self.widget("migrate-secure").set_active(False)

        downtime_box = self.widget("migrate-maxdowntime-box")
        support_downtime = self.vm.support_downtime()
        downtime_tooltip = ""
        if not support_downtime:
            downtime_tooltip = _("Libvirt version does not support setting "
                                 "downtime.")
        downtime_box.set_sensitive(support_downtime)
        downtime_box.set_tooltip_text(downtime_tooltip)

        if self.conn.is_xen():
            # Default xen port is 8002
            self.widget("migrate-port").set_value(8002)
        else:
            # QEMU migrate port range is 49152+64
            self.widget("migrate-port").set_value(49152)

        secure_box = self.widget("migrate-secure-box")
        support_secure = hasattr(libvirt, "VIR_MIGRATE_TUNNELLED")
        secure_tooltip = ""
        if not support_secure:
            secure_tooltip = _("Libvirt version does not support tunnelled "
                               "migration.")

        secure_box.set_sensitive(support_secure)
        secure_box.set_tooltip_text(secure_tooltip)

        self.rebuild_dest_rows()

    def set_state(self, vm):
        self.vm = vm
        self.conn = vm.conn
        self.reset_state()

    def destconn_changed(self, src):
        active = src.get_active()
        tooltip = ""
        if active == -1:
            tooltip = _("A valid destination connection must be selected.")

        self.widget("migrate-finish").set_sensitive(active != -1)
        self.widget("migrate-finish").set_tooltip_text(tooltip)

    def toggle_set_rate(self, src):
        enable = src.get_active()
        self.widget("migrate-rate").set_sensitive(enable)

    def toggle_set_interface(self, src):
        enable = src.get_active()
        port_enable = self.widget("migrate-set-port").get_active()
        self.widget("migrate-interface").set_sensitive(enable)
        self.widget("migrate-set-port").set_sensitive(enable)
        self.widget("migrate-port").set_sensitive(enable and port_enable)

    def toggle_set_maxdowntime(self, src):
        enable = src.get_active()
        self.widget("migrate-max-downtime").set_sensitive(enable)

    def toggle_set_port(self, src):
        enable = src.get_active()
        self.widget("migrate-port").set_sensitive(enable)

    def get_config_destconn(self):
        combo = self.widget("migrate-dest")
        model = combo.get_model()

        idx = combo.get_active()
        if idx == -1:
            return None

        row = model[idx]
        if not row[2]:
            return None

        return row[1]

    def get_config_offline(self):
        return self.widget("migrate-offline").get_active()

    def get_config_max_downtime(self):
        if not self.get_config_max_downtime_enabled():
            return 0
        return int(self.widget("migrate-max-downtime").get_value())

    def get_config_secure(self):
        return self.widget("migrate-secure").get_active()

    def get_config_max_downtime_enabled(self):
        return self.widget("migrate-max-downtime").get_sensitive()

    def get_config_rate_enabled(self):
        return self.widget("migrate-rate").get_sensitive()
    def get_config_rate(self):
        if not self.get_config_rate_enabled():
            return 0
        return int(self.widget("migrate-rate").get_value())

    def get_config_interface_enabled(self):
        return self.widget("migrate-interface").get_sensitive()
    def get_config_interface(self):
        if not self.get_config_interface_enabled():
            return None
        return self.widget("migrate-interface").get_text()

    def get_config_port_enabled(self):
        return self.widget("migrate-port").get_sensitive()
    def get_config_port(self):
        if not self.get_config_port_enabled():
            return 0
        return int(self.widget("migrate-port").get_value())

    def build_localhost_uri(self, destconn, srcuri):
        desthost = destconn.get_qualified_hostname()
        if desthost == "localhost":
            # We couldn't find a host name for the destination machine
            # that is accessible from the source machine.
            # /etc/hosts is likely borked and the only hostname it will
            # give us is localhost. Remember, the dest machine can actually
            # be our local machine so we may not already know its hostname
            raise RuntimeError(_("Could not determine remotely accessible "
                                 "hostname for destination connection."))

        # Since the connection started as local, we have no clue about
        # how to access it remotely. Assume users have a uniform access
        # setup and use the same credentials as the remote source URI
        return self.edit_uri(srcuri, desthost, None)

    def edit_uri(self, uri, hostname, port):
        split = list(uriutil.uri_split(uri))

        hostname = hostname or split[2]
        if port:
            if hostname.count(":"):
                hostname = hostname.split(":")[0]
            hostname += ":%s" % port

        split[2] = hostname
        return uri_join(tuple(split))

    def build_migrate_uri(self, destconn, srcuri):
        conn = self.conn

        interface = self.get_config_interface()
        port = self.get_config_port()
        secure = self.get_config_secure()

        if not interface and not secure:
            return None

        if secure:
            # P2P migration uri is a libvirt connection uri, e.g.
            # qemu+ssh://root@foobar/system

            # For secure migration, we need to make sure we aren't migrating
            # to the local connection, because libvirt will pull try to use
            # 'qemu:///system' as the migrate URI which will deadlock
            if destconn.is_local():
                uri = self.build_localhost_uri(destconn, srcuri)
            else:
                uri = destconn.get_uri()

            uri = self.edit_uri(uri, interface, port)

        else:
            # Regular migration URI is HV specific
            uri = ""
            if conn.is_xen():
                uri = "xenmigr://%s" % interface

            else:
                uri = "tcp:%s" % interface

            if port:
                uri += ":%s" % port

        return uri or None

    def rebuild_dest_rows(self):
        newrows = []

        for row in self.destconn_rows:
            newrows.append(self.build_dest_row(row[1]))

        self.destconn_rows = newrows
        self.populate_dest_combo()

    def populate_dest_combo(self):
        combo = self.widget("migrate-dest")
        model = combo.get_model()
        idx = combo.get_active()
        idxconn = None
        if idx != -1:
            idxconn = model[idx][1]

        rows = [[_("No connections available."), None, False, None]]
        if self.destconn_rows:
            rows = self.destconn_rows

        model.clear()
        for r in rows:
            # Don't list the current connection
            if r[1] == self.conn:
                continue
            model.append(r)

        # Find old index
        idx = -1
        for i in range(len(model)):
            row = model[i]
            conn = row[1]

            if idxconn:
                if conn == idxconn and row[2]:
                    idx = i
                    break
            else:
                if row[2]:
                    idx = i
                    break

        combo.set_active(idx)

    def dest_add_conn(self, engine_ignore, conn):
        combo = self.widget("migrate-dest")
        model = combo.get_model()

        newrow = self.build_dest_row(conn)

        # Make sure connection isn't already present
        for row in model:
            if row[1] and row[1].get_uri() == newrow[1].get_uri():
                return

        conn.connect("state-changed", self.destconn_state_changed)
        self.destconn_rows.append(newrow)
        self.populate_dest_combo()

    def dest_remove_conn(self, engine_ignore, uri):
        # Make sure connection isn't already present
        for row in self.destconn_rows:
            if row[1] and row[1].get_uri() == uri:
                self.destconn_rows.remove(row)

        self.populate_dest_combo()

    def destconn_state_changed(self, conn):
        for row in self.destconn_rows:
            if row[1] == conn:
                self.destconn_rows.remove(row)
                self.destconn_rows.append(self.build_dest_row(conn))

        self.populate_dest_combo()

    def build_dest_row(self, destconn):
        driver = self.conn.get_driver()
        origuri = self.conn.get_uri()

        can_migrate = False
        desc = destconn.get_pretty_desc_inactive()
        reason = ""
        desturi = destconn.get_uri()

        if destconn.get_driver() != driver:
            reason = _("Connection hypervisors do not match.")
        elif destconn.get_state() == destconn.STATE_DISCONNECTED:
            reason = _("Connection is disconnected.")
        elif destconn.get_uri() == origuri:
            # Same connection
            pass
        elif destconn.get_state() == destconn.STATE_ACTIVE:
            # Assumably we can migrate to this connection
            can_migrate = True
            reason = desturi

        return [desc, destconn, can_migrate, reason]


    def validate(self):
        interface = self.get_config_interface()
        rate = self.get_config_rate()
        port = self.get_config_port()
        max_downtime = self.get_config_max_downtime()

        if self.get_config_max_downtime_enabled() and max_downtime == 0:
            return self.err.val_err(_("max downtime must be greater than 0."))

        if self.get_config_interface_enabled() and interface is None:
            return self.err.val_err(_("An interface must be specified."))

        if self.get_config_rate_enabled() and rate == 0:
            return self.err.val_err(_("Transfer rate must be greater than 0."))

        if self.get_config_port_enabled() and port == 0:
            return self.err.val_err(_("Port must be greater than 0."))

        return True

    def finish(self, src_ignore):
        try:
            if not self.validate():
                return

            destconn = self.get_config_destconn()
            srcuri = self.vm.conn.get_uri()
            srchost = self.vm.conn.get_hostname()
            dsthost = destconn.get_qualified_hostname()
            max_downtime = self.get_config_max_downtime()
            live = not self.get_config_offline()
            secure = self.get_config_secure()
            uri = self.build_migrate_uri(destconn, srcuri)
            rate = self.get_config_rate()
            if rate:
                rate = int(rate)
        except Exception, e:
            details = "".join(traceback.format_exc())
            self.err.show_err((_("Uncaught error validating input: %s") %
                               str(e)),
                               details=details)
            return

        self.topwin.set_sensitive(False)
        self.topwin.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH))

        cancel_cb = None
        if self.vm.getjobinfo_supported:
            cancel_cb = (self.cancel_migration, self.vm)

        progWin = vmmAsyncJob(self._async_migrate,
                              [self.vm, destconn, uri, rate, live, secure,
                               max_downtime],
                              _("Migrating VM '%s'" % self.vm.get_name()),
                              (_("Migrating VM '%s' from %s to %s. "
                                 "This may take a while.") %
                                (self.vm.get_name(), srchost, dsthost)),
                              self.topwin, cancel_cb=cancel_cb)
        error, details = progWin.run()

        self.topwin.set_sensitive(True)
        self.topwin.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.TOP_LEFT_ARROW))

        if error:
            error = _("Unable to migrate guest: %s") % error
            self.err.show_err(error,
                              details=details)
        else:
            self.conn.tick(noStatsUpdate=True)
            destconn.tick(noStatsUpdate=True)
            self.close()

    def _async_set_max_downtime(self, vm, max_downtime, migrate_thread):
        if not migrate_thread.isAlive():
            return False
        try:
            vm.migrate_set_max_downtime(max_downtime, 0)
            return False
        except libvirt.libvirtError, e:
            if (isinstance(e, libvirt.libvirtError) and
                e.get_error_code() == libvirt.VIR_ERR_OPERATION_INVALID):
                # migration has not been started, wait 100 milliseconds
                return True

            logging.warning("Error setting migrate downtime: %s", e)
            return False

    def cancel_migration(self, asyncjob, vm):
        logging.debug("Cancelling migrate job")
        if not vm:
            return

        try:
            vm.abort_job()
        except Exception, e:
            logging.exception("Error cancelling migrate job")
            asyncjob.show_warning(_("Error cancelling migrate job: %s") % e)
            return

        asyncjob.job_canceled = True
        return

    def _async_migrate(self, asyncjob,
                       origvm, origdconn, migrate_uri, rate, live,
                       secure, max_downtime):
        meter = asyncjob.get_meter()

        srcconn = util.dup_conn(origvm.conn)
        dstconn = util.dup_conn(origdconn)

        vminst = srcconn.vmm.lookupByName(origvm.get_name())
        vm = vmmDomain(srcconn, vminst, vminst.UUID())

        logging.debug("Migrating vm=%s from %s to %s", vm.get_name(),
                      srcconn.get_uri(), dstconn.get_uri())

        timer = None
        if max_downtime != 0:
            # 0 means that the spin box migrate-max-downtime does not
            # be enabled.
            current_thread = threading.currentThread()
            timer = self.timeout_add(100, self._async_set_max_downtime,
                                     vm, max_downtime, current_thread)

        vm.migrate(dstconn, migrate_uri, rate, live, secure, meter=meter)
        if timer:
            self.idle_add(GLib.source_remove, timer)
