# -*- coding: utf-8 -*-
#
# Copyright (C) 2006-2008 Red Hat, Inc.
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
# Copyright (C) 2010 Marc-André Lureau <marcandre.lureau@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.
#

# pylint: disable=E0611
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GtkVnc
from gi.repository import SpiceClientGtk
from gi.repository import SpiceClientGLib
# pylint: enable=E0611

import libvirt

import os
import signal
import socket
import logging

import virtManager.uihelpers as uihelpers
from virtManager.autodrawer import AutoDrawer
from virtManager.baseclass import vmmGObjectUI, vmmGObject

# Console pages
PAGE_UNAVAILABLE = 0
PAGE_AUTHENTICATE = 1
PAGE_VIEWER = 2


def has_property(obj, setting):
    try:
        obj.get_property(setting)
    except TypeError:
        return False
    return True


class ConnectionInfo(object):
    """
    Holds all the bits needed to make a connection to a graphical console
    """
    def __init__(self, conn, gdev):
        self.gtype      = gdev.type
        self.gport      = gdev.port and str(gdev.port) or None
        self.gsocket    = gdev.socket
        self.gaddr      = gdev.listen or "127.0.0.1"

        self.transport, self.connuser = conn.get_transport()
        self._connhost = conn.get_uri_hostname() or "127.0.0.1"

        self._connport = None
        if self._connhost.count(":"):
            self._connhost, self._connport = self._connhost.split(":", 1)

    def need_tunnel(self):
        if self.gaddr != "127.0.0.1":
            return False

        return self.transport in ["ssh", "ext"]

    def get_conn_host(self):
        host = self._connhost
        port = self._connport

        if not self.need_tunnel():
            port = self.gport
            if self.gaddr != "0.0.0.0":
                host = self.gaddr

        return host, port

    def logstring(self):
        return ("proto=%s trans=%s connhost=%s connuser=%s "
                "connport=%s gaddr=%s gport=%s gsocket=%s" %
                (self.gtype, self.transport, self._connhost, self.connuser,
                 self._connport, self.gaddr, self.gport, self.gsocket))
    def console_active(self):
        if self.gsocket:
            return True
        if not self.gport:
            return False
        return int(self.gport) == -1


class Tunnel(object):
    def __init__(self):
        self.outfd = None
        self.errfd = None
        self.pid = None

    def open(self, ginfo):
        if self.outfd is not None:
            return -1

        host, port = ginfo.get_conn_host()

        # Build SSH cmd
        argv = ["ssh", "ssh"]
        if port:
            argv += ["-p", str(port)]

        if ginfo.connuser:
            argv += ['-l', ginfo.connuser]

        argv += [host]

        # Build 'nc' command run on the remote host
        #
        # This ugly thing is a shell script to detect availability of
        # the -q option for 'nc': debian and suse based distros need this
        # flag to ensure the remote nc will exit on EOF, so it will go away
        # when we close the VNC tunnel. If it doesn't go away, subsequent
        # VNC connection attempts will hang.
        #
        # Fedora's 'nc' doesn't have this option, and apparently defaults
        # to the desired behavior.
        #
        if ginfo.gsocket:
            nc_params = "-U %s" % ginfo.gsocket
        else:
            nc_params = "%s %s" % (ginfo.gaddr, ginfo.gport)

        nc_cmd = (
            """nc -q 2>&1 | grep "requires an argument" >/dev/null;"""
            """if [ $? -eq 0 ] ; then"""
            """   CMD="nc -q 0 %(nc_params)s";"""
            """else"""
            """   CMD="nc %(nc_params)s";"""
            """fi;"""
            """eval "$CMD";""" %
            {'nc_params': nc_params})

        argv.append("sh -c")
        argv.append("'%s'" % nc_cmd)

        argv_str = reduce(lambda x, y: x + " " + y, argv[1:])
        logging.debug("Creating SSH tunnel: %s", argv_str)

        fds      = socket.socketpair()
        errorfds = socket.socketpair()

        pid = os.fork()
        if pid == 0:
            fds[0].close()
            errorfds[0].close()

            os.close(0)
            os.close(1)
            os.close(2)
            os.dup(fds[1].fileno())
            os.dup(fds[1].fileno())
            os.dup(errorfds[1].fileno())
            os.execlp(*argv)
            os._exit(1)  # pylint: disable=W0212
        else:
            fds[1].close()
            errorfds[1].close()

        logging.debug("Tunnel PID=%d OUTFD=%d ERRFD=%d",
                      pid, fds[0].fileno(), errorfds[0].fileno())
        errorfds[0].setblocking(0)

        self.outfd = fds[0]
        self.errfd = errorfds[0]
        self.pid = pid

        fd = fds[0].fileno()
        if fd < 0:
            raise SystemError("can't open a new tunnel: fd=%d" % fd)
        return fd

    def close(self):
        if self.outfd is None:
            return

        logging.debug("Shutting down tunnel PID=%d OUTFD=%d ERRFD=%d",
                      self.pid, self.outfd.fileno(),
                      self.errfd.fileno())
        self.outfd.close()
        self.outfd = None
        self.errfd.close()
        self.errfd = None

        os.kill(self.pid, signal.SIGKILL)
        os.waitpid(self.pid, 0)
        self.pid = None

    def get_err_output(self):
        errout = ""
        while True:
            try:
                new = self.errfd.recv(1024)
            except:
                break

            if not new:
                break

            errout += new

        return errout


class Tunnels(object):
    def __init__(self, ginfo):
        self.ginfo = ginfo
        self._tunnels = []

    def open_new(self):
        t = Tunnel()
        fd = t.open(self.ginfo)
        self._tunnels.append(t)
        return fd

    def close_all(self):
        for l in self._tunnels:
            l.close()

    def get_err_output(self):
        errout = ""
        for l in self._tunnels:
            errout += l.get_err_output()
        return errout


class Viewer(vmmGObject):
    def __init__(self, console):
        vmmGObject.__init__(self)
        self.console = console
        self.display = None

    def close(self):
        raise NotImplementedError()

    def _cleanup(self):
        self.close()

        if self.display:
            self.display.destroy()
        self.display = None
        self.console = None

    def get_pixbuf(self):
        return self.display.get_pixbuf()

    def get_grab_keys(self):
        raise NotImplementedError()

    def set_grab_keys(self):
        raise NotImplementedError()

    def send_keys(self, keys):
        raise NotImplementedError()

    def open_host(self, ginfo, password=None):
        raise NotImplementedError()

    def open_fd(self, fd, password=None):
        raise NotImplementedError()

    def get_desktop_resolution(self):
        raise NotImplementedError()


class VNCViewer(Viewer):
    def __init__(self, console):
        Viewer.__init__(self, console)
        self.display = GtkVnc.Display.new()
        self.sockfd = None

        # Last noticed desktop resolution
        self.desktop_resolution = None

    def init_widget(self):
        self.set_grab_keys()

        self.display.realize()

        # Make sure viewer doesn't force resize itself
        self.display.set_force_size(False)

        self.console.refresh_scaling()

        self.display.set_keyboard_grab(True)
        self.display.set_pointer_grab(True)

        self.display.connect("vnc-pointer-grab", self.console.pointer_grabbed)
        self.display.connect("vnc-pointer-ungrab", self.console.pointer_ungrabbed)
        self.display.connect("vnc-auth-credential", self._auth_credential)
        self.display.connect("vnc-initialized",
                             lambda src: self.console.connected())
        self.display.connect("vnc-disconnected",
                             lambda src: self.console.disconnected())
        self.display.connect("vnc-desktop-resize", self._desktop_resize)
        self.display.connect("focus-in-event", self.console.viewer_focus_changed)
        self.display.connect("focus-out-event", self.console.viewer_focus_changed)

        self.display.show()

    def get_grab_keys(self):
        return self.display.get_grab_keys().as_string()

    def set_grab_keys(self):
        try:
            keys = self.config.get_keys_combination()
            if not keys:
                return

            try:
                keys = [int(k) for k in keys.split(',')]
            except:
                logging.debug("Error in grab_keys configuration in GConf",
                              exc_info=True)
                return

            seq = GtkVnc.GrabSequence.new(keys)
            self.display.set_grab_keys(seq)
        except Exception, e:
            logging.debug("Error when getting the grab keys combination: %s",
                          str(e))

    def send_keys(self, keys):
        return self.display.send_keys([Gdk.keyval_from_name(k) for k in keys])

    def _desktop_resize(self, src_ignore, w, h):
        self.desktop_resolution = (w, h)
        self.console.widget("console-gfx-scroll").queue_resize()

    def get_desktop_resolution(self):
        return self.desktop_resolution

    def _auth_credential(self, src_ignore, credList):
        values = []
        for idx in range(int(credList.n_values)):
            values.append(credList.get_nth(idx))

        for cred in values:
            if cred in [GtkVnc.DisplayCredential.PASSWORD,
                        GtkVnc.DisplayCredential.USERNAME,
                        GtkVnc.DisplayCredential.CLIENTNAME]:
                continue

            self.console.err.show_err(
                summary=_("Unable to provide requested credentials to "
                          "the VNC server"),
                details=(_("The credential type %s is not supported") %
                         (str(cred))),
                title=_("Unable to authenticate"),
                async=True)

            # schedule_retry will error out
            self.console.viewerRetriesScheduled = 10
            self.close()
            self.console.activate_unavailable_page(
                            _("Unsupported console authentication type"))
            return

        withUsername = False
        withPassword = False
        for cred in values:
            logging.debug("Got credential request %s", cred)
            if cred == GtkVnc.DisplayCredential.PASSWORD:
                withPassword = True
            elif cred == GtkVnc.DisplayCredential.USERNAME:
                withUsername = True
            elif cred == GtkVnc.DisplayCredential.CLIENTNAME:
                self.display.set_credential(cred, "libvirt-vnc")

        if withUsername or withPassword:
            self.console.activate_auth_page(withPassword, withUsername)

    def get_scaling(self):
        return self.display.get_scaling()

    def set_scaling(self, scaling):
        return self.display.set_scaling(scaling)

    def close(self):
        self.display.close()
        if not self.sockfd:
            return

        self.sockfd.close()
        self.sockfd = None

    def is_open(self):
        return self.display.is_open()

    def open_host(self, ginfo, password=None):
        host, port = ginfo.get_conn_host()

        if not ginfo.gsocket:
            logging.debug("VNC connection to %s:%s", host, port)
            self.display.open_host(host, port)
            return

        logging.debug("VNC connecting to socket=%s", ginfo.gsocket)
        try:
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            sock.connect(ginfo.gsocket)
            self.sockfd = sock
        except Exception, e:
            raise RuntimeError(_("Error opening socket path '%s': %s") %
                               (ginfo.gsocket, e))

        fd = self.sockfd.fileno()
        if fd < 0:
            raise RuntimeError((_("Error opening socket path '%s'") %
                                ginfo.gsocket) + " fd=%s" % fd)
        self.open_fd(fd)

    def open_fd(self, fd, password=None):
        ignore = password
        self.display.open_fd(fd)

    def set_credential_username(self, cred):
        self.display.set_credential(GtkVnc.DisplayCredential.USERNAME, cred)

    def set_credential_password(self, cred):
        self.display.set_credential(GtkVnc.DisplayCredential.PASSWORD, cred)


class SpiceViewer(Viewer):
    def __init__(self, console):
        Viewer.__init__(self, console)
        self.spice_session = None
        self.display = None
        self.audio = None
        self.display_channel = None

    def _init_widget(self):
        self.set_grab_keys()
        self.console.refresh_scaling()

        self.display.realize()
        self.display.connect("mouse-grab", lambda src, g: g and self.console.pointer_grabbed(src))
        self.display.connect("mouse-grab", lambda src, g: g or self.console.pointer_ungrabbed(src))

        self.display.connect("focus-in-event",
                             self.console.viewer_focus_changed)
        self.display.connect("focus-out-event",
                             self.console.viewer_focus_changed)

        self.display.show()

    def get_grab_keys(self):
        return self.display.get_grab_keys().as_string()

    def set_grab_keys(self):
        try:
            keys = self.config.get_keys_combination()
            if not keys:
                return

            try:
                keys = [int(k) for k in keys.split(',')]
            except:
                logging.debug("Error in grab_keys configuration in GConf",
                              exc_info=True)
                return

            seq = SpiceClientGtk.GrabSequence.new(keys)
            self.display.set_grab_keys(seq)
        except Exception, e:
            logging.debug("Error when getting the grab keys combination: %s",
                          str(e))

    def send_keys(self, keys):
        return self.display.send_keys([Gdk.keyval_from_name(k) for k in keys],
                                      SpiceClientGtk.DisplayKeyEvent.CLICK)

    def close(self):
        if self.spice_session is not None:
            self.spice_session.disconnect()
        self.spice_session = None
        self.audio = None
        if self.display:
            self.display.destroy()
        self.display = None
        self.display_channel = None

    def is_open(self):
        return self.spice_session is not None

    def _main_channel_event_cb(self, channel, event):
        if event == SpiceClientGLib.ChannelEvent.CLOSED:
            if self.console:
                self.console.disconnected()
        elif event == SpiceClientGLib.ChannelEvent.ERROR_AUTH:
            if self.console:
                self.console.activate_auth_page()

    def _channel_open_fd_request(self, channel, tls_ignore):
        if not self.console.tunnels:
            raise SystemError("Got fd request with no configured tunnel!")

        logging.debug("Opening tunnel for channel: %s", channel)
        fd = self.console.tunnels.open_new()
        channel.open_fd(fd)

    def _channel_new_cb(self, session, channel):
        GObject.GObject.connect(channel, "open-fd",
                                self._channel_open_fd_request)

        if type(channel) == SpiceClientGLib.MainChannel:
            channel.connect_after("channel-event", self._main_channel_event_cb)
            return

        if type(channel) == SpiceClientGLib.DisplayChannel:
            channel_id = channel.get_property("channel-id")

            if channel_id != 0:
                logging.debug("Spice multi-head unsupported")
                return

            self.display_channel = channel
            self.display = SpiceClientGtk.Display.new(self.spice_session,
                                                      channel_id)
            self.console.widget("console-gfx-viewport").add(self.display)
            self._init_widget()
            self.console.connected()
            return

        if (type(channel) in [SpiceClientGLib.PlaybackChannel,
                              SpiceClientGLib.RecordChannel] and
            not self.audio):
            self.audio = SpiceClientGLib.Audio.get(self.spice_session, None)
            return

    def get_desktop_resolution(self):
        if (not self.display_channel or
            not has_property(self.display_channel, "width")):
            return None
        return self.display_channel.get_properties("width", "height")

    def open_host(self, ginfo, password=None):
        host, port = ginfo.get_conn_host()

        uri = "spice://"
        uri += str(host) + "?port=" + str(port)
        logging.debug("spice uri: %s", uri)

        self.spice_session = SpiceClientGLib.Session()
        self.spice_session.set_property("uri", uri)
        if password:
            self.spice_session.set_property("password", password)
        GObject.GObject.connect(self.spice_session, "channel-new",
                                self._channel_new_cb)
        self.spice_session.connect()

    def open_fd(self, fd, password=None):
        self.spice_session = SpiceClientGLib.Session()
        if password:
            self.spice_session.set_property("password", password)
        GObject.GObject.connect(self.spice_session, "channel-new",
                                self._channel_new_cb)
        self.spice_session.open_fd(fd)

    def set_credential_password(self, cred):
        self.spice_session.set_property("password", cred)
        if self.console.tunnels:
            fd = self.console.tunnels.open_new()
            self.spice_session.open_fd(fd)
        else:
            self.spice_session.connect()

    def get_scaling(self):
        if not has_property(self.display, "scaling"):
            return False
        return self.display.get_property("scaling")

    def set_scaling(self, scaling):
        if not has_property(self.display, "scaling"):
            logging.debug("Spice version doesn't support scaling.")
            return
        self.display.set_property("scaling", scaling)


class vmmConsolePages(vmmGObjectUI):
    def __init__(self, vm, builder, topwin):
        vmmGObjectUI.__init__(self, None, None, builder=builder, topwin=topwin)

        self.vm = vm
        self.pointer_is_grabbed = False
        self.change_title()
        self.vm.connect("config-changed", self.change_title)

        # State for disabling modifiers when keyboard is grabbed
        self.accel_groups = Gtk.accel_groups_from_object(self.topwin)
        self.gtk_settings_accel = None
        self.gtk_settings_mnemonic = None

        # Initialize display widget
        self.viewer = None
        self.tunnels = None
        self.viewerRetriesScheduled = 0
        self.viewerRetryDelay = 125
        self._viewer_connected = False
        self.viewer_connecting = False
        self.scale_type = self.vm.get_console_scaling()

        # Fullscreen toolbar
        self.send_key_button = None
        self.fs_toolbar = None
        self.fs_drawer = None
        self.keycombo_menu = uihelpers.build_keycombo_menu(self.send_key)
        self.init_fs_toolbar()

        finish_img = Gtk.Image.new_from_stock(Gtk.STOCK_YES,
                                              Gtk.IconSize.BUTTON)
        self.widget("console-auth-login").set_image(finish_img)

        # Make viewer widget background always be black
        black = Gdk.Color(0, 0, 0)
        self.widget("console-gfx-viewport").modify_bg(Gtk.StateType.NORMAL,
                                                      black)

        # Signals are added by vmmDetails. Don't use connect_signals here
        # or it changes will be overwritten

        self.refresh_scaling()
        self.add_gconf_handle(
            self.vm.on_console_scaling_changed(self.refresh_scaling))

        scroll = self.widget("console-gfx-scroll")
        scroll.connect("size-allocate", self.scroll_size_allocate)
        self.add_gconf_handle(
            self.config.on_console_accels_changed(self.set_enable_accel))
        self.add_gconf_handle(
            self.config.on_keys_combination_changed(self.grab_keys_changed))

        self.page_changed()

    def is_visible(self):
        if self.topwin:
            return self.topwin.get_visible()
        else:
            return False

    def _cleanup(self):
        self.vm = None

        if self.viewer:
            self.viewer.cleanup()
        self.viewer = None

        self.keycombo_menu.destroy()
        self.keycombo_menu = None
        self.fs_drawer.destroy()
        self.fs_drawer = None
        self.fs_toolbar.destroy()
        self.fs_toolbar = None

    ##########################
    # Initialization helpers #
    ##########################

    def init_fs_toolbar(self):
        scroll = self.widget("console-gfx-scroll")
        pages = self.widget("console-pages")
        pages.remove(scroll)

        self.fs_toolbar = Gtk.Toolbar()
        self.fs_toolbar.set_show_arrow(False)
        self.fs_toolbar.set_no_show_all(True)
        self.fs_toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)

        # Exit fullscreen button
        button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_LEAVE_FULLSCREEN)
        button.set_tooltip_text(_("Leave fullscreen"))
        button.show()
        self.fs_toolbar.add(button)
        button.connect("clicked", self.leave_fullscreen)

        def keycombo_menu_clicked(src):
            ignore = src
            def menu_location(menu, toolbar):
                ignore = menu
                ignore, x, y = toolbar.get_window().get_origin()
                height = toolbar.get_window().get_height()

                return x, y + height, True

            self.keycombo_menu.popup(None, None, menu_location,
                                     self.fs_toolbar, 0,
                                     Gtk.get_current_event_time())

        self.send_key_button = Gtk.ToolButton()
        self.send_key_button.set_icon_name(
                                "preferences-desktop-keyboard-shortcuts")
        self.send_key_button.set_tooltip_text(_("Send key combination"))
        self.send_key_button.show_all()
        self.send_key_button.connect("clicked", keycombo_menu_clicked)
        self.fs_toolbar.add(self.send_key_button)

        self.fs_drawer = AutoDrawer()
        self.fs_drawer.set_active(False)
        self.fs_drawer.set_over(self.fs_toolbar)
        self.fs_drawer.set_under(scroll)
        self.fs_drawer.set_offset(-1)
        self.fs_drawer.set_fill(False)
        self.fs_drawer.set_overlap_pixels(1)
        self.fs_drawer.set_nooverlap_pixels(0)
        self.fs_drawer.show_all()

        pages.add(self.fs_drawer)

    def change_title(self, ignore1=None):
        title = self.vm.get_name() + " " + _("Virtual Machine")

        if self.pointer_is_grabbed and self.viewer:
            keystr = self.viewer.get_grab_keys()
            keymsg = _("Press %s to release pointer.") % keystr

            title = keymsg + " " + title

        self.topwin.set_title(title)

    def viewer_focus_changed(self, ignore1=None, ignore2=None):
        has_focus = (self.viewer and
                     self.viewer.display and
                     self.viewer.display.get_property("has-focus"))
        force_accel = self.config.get_console_accels()

        if force_accel:
            self._enable_modifiers()
        elif has_focus and self.viewer_connected:
            self._disable_modifiers()
        else:
            self._enable_modifiers()

    def pointer_grabbed(self, src_ignore):
        self.pointer_is_grabbed = True
        self.change_title()

    def pointer_ungrabbed(self, src_ignore):
        self.pointer_is_grabbed = False
        self.change_title()

    def _disable_modifiers(self):
        if self.gtk_settings_accel is not None:
            return

        for g in self.accel_groups:
            self.topwin.remove_accel_group(g)

        settings = Gtk.Settings.get_default()
        self.gtk_settings_accel = settings.get_property('gtk-menu-bar-accel')
        settings.set_property('gtk-menu-bar-accel', None)

        if has_property(settings, "gtk-enable-mnemonics"):
            self.gtk_settings_mnemonic = settings.get_property(
                                                        "gtk-enable-mnemonics")
            settings.set_property("gtk-enable-mnemonics", False)

    def _enable_modifiers(self):
        if self.gtk_settings_accel is None:
            return

        settings = Gtk.Settings.get_default()
        settings.set_property('gtk-menu-bar-accel', self.gtk_settings_accel)
        self.gtk_settings_accel = None

        if self.gtk_settings_mnemonic is not None:
            settings.set_property("gtk-enable-mnemonics",
                                  self.gtk_settings_mnemonic)

        for g in self.accel_groups:
            self.topwin.add_accel_group(g)

    def grab_keys_changed(self):
        self.viewer.set_grab_keys()

    def set_enable_accel(self):
        # Make sure modifiers are up to date
        self.viewer_focus_changed()

    def refresh_scaling(self):
        self.scale_type = self.vm.get_console_scaling()
        self.widget("details-menu-view-scale-always").set_active(
            self.scale_type == self.config.CONSOLE_SCALE_ALWAYS)
        self.widget("details-menu-view-scale-never").set_active(
            self.scale_type == self.config.CONSOLE_SCALE_NEVER)
        self.widget("details-menu-view-scale-fullscreen").set_active(
            self.scale_type == self.config.CONSOLE_SCALE_FULLSCREEN)

        self.update_scaling()

    def set_scale_type(self, src):
        if not src.get_active():
            return

        if src == self.widget("details-menu-view-scale-always"):
            self.scale_type = self.config.CONSOLE_SCALE_ALWAYS
        elif src == self.widget("details-menu-view-scale-fullscreen"):
            self.scale_type = self.config.CONSOLE_SCALE_FULLSCREEN
        elif src == self.widget("details-menu-view-scale-never"):
            self.scale_type = self.config.CONSOLE_SCALE_NEVER

        self.vm.set_console_scaling(self.scale_type)
        self.update_scaling()

    def update_scaling(self):
        if not self.viewer:
            return

        curscale = self.viewer.get_scaling()
        fs = self.widget("control-fullscreen").get_active()
        vnc_scroll = self.widget("console-gfx-scroll")

        if (self.scale_type == self.config.CONSOLE_SCALE_NEVER
            and curscale is True):
            self.viewer.set_scaling(False)
        elif (self.scale_type == self.config.CONSOLE_SCALE_ALWAYS
              and curscale is False):
            self.viewer.set_scaling(True)
        elif (self.scale_type == self.config.CONSOLE_SCALE_FULLSCREEN
              and curscale != fs):
            self.viewer.set_scaling(fs)

        # Refresh viewer size
        vnc_scroll.queue_resize()

    def auth_login(self, ignore):
        self.set_credentials()
        self.activate_viewer_page()

    def toggle_fullscreen(self, src):
        do_fullscreen = src.get_active()
        self._change_fullscreen(do_fullscreen)

    def leave_fullscreen(self, ignore=None):
        self._change_fullscreen(False)

    def _change_fullscreen(self, do_fullscreen):
        self.widget("control-fullscreen").set_active(do_fullscreen)

        if do_fullscreen:
            self.topwin.fullscreen()
            self.fs_toolbar.show()
            self.fs_drawer.set_active(True)
            self.widget("toolbar-box").hide()
            self.widget("details-menubar").hide()
        else:
            self.fs_toolbar.hide()
            self.fs_drawer.set_active(False)
            self.topwin.unfullscreen()

            if self.widget("details-menu-view-toolbar").get_active():
                self.widget("toolbar-box").show()
            self.widget("details-menubar").show()

        self.update_scaling()

    def size_to_vm(self, src_ignore):
        # Resize the console to best fit the VM resolution
        if not self.viewer:
            return
        if not self.viewer.get_desktop_resolution():
            return

        w, h = self.viewer.get_desktop_resolution()
        self.topwin.unmaximize()
        self.topwin.resize(1, 1)
        self.queue_scroll_resize_helper(w, h)

    def send_key(self, src, keys):
        ignore = src

        if keys is not None:
            self.viewer.send_keys(keys)


    ##########################
    # State tracking methods #
    ##########################

    def view_vm_status(self):
        if not self.vm:
            # window has been closed and no pages to update are available.
            return
        status = self.vm.status()
        if status == libvirt.VIR_DOMAIN_SHUTOFF:
            self.activate_unavailable_page(_("Guest not running"))
        else:
            if status == libvirt.VIR_DOMAIN_CRASHED:
                self.activate_unavailable_page(_("Guest has crashed"))

    def close_viewer(self):
        if self.viewer is None:
            return

        v = self.viewer  # close_viewer() can be reentered
        self.viewer = None
        w = v.display

        viewport = self.widget("console-gfx-viewport")
        if w and w in viewport.get_children():
            viewport.remove(w)

        v.close()
        self.viewer_connected = False
        self.leave_fullscreen()

    def update_widget_states(self, vm, status_ignore):
        runable = vm.is_runable()
        pages   = self.widget("console-pages")
        page    = pages.get_current_page()

        if runable:
            if page != PAGE_UNAVAILABLE:
                pages.set_current_page(PAGE_UNAVAILABLE)

            self.view_vm_status()
            return

        elif page in [PAGE_UNAVAILABLE, PAGE_VIEWER]:
            if self.viewer and self.viewer.is_open():
                self.activate_viewer_page()
            else:
                self.viewerRetriesScheduled = 0
                self.viewerRetryDelay = 125
                self.try_login()

        return

    ###################
    # Page Navigation #
    ###################

    def activate_unavailable_page(self, msg):
        """
        This function is passed to serialcon.py at least, so change
        with care
        """
        self.close_viewer()
        self.widget("console-pages").set_current_page(PAGE_UNAVAILABLE)
        self.widget("details-menu-vm-screenshot").set_sensitive(False)
        self.widget("console-unavailable").set_label("<b>" + msg + "</b>")

    def activate_auth_page(self, withPassword=True, withUsername=False):
        (pw, username) = self.config.get_console_password(self.vm)
        self.widget("details-menu-vm-screenshot").set_sensitive(False)

        if withPassword:
            self.widget("console-auth-password").show()
            self.widget("label-auth-password").show()
        else:
            self.widget("console-auth-password").hide()
            self.widget("label-auth-password").hide()

        if withUsername:
            self.widget("console-auth-username").show()
            self.widget("label-auth-username").show()
        else:
            self.widget("console-auth-username").hide()
            self.widget("label-auth-username").hide()

        self.widget("console-auth-username").set_text(username)
        self.widget("console-auth-password").set_text(pw)

        if self.config.has_keyring():
            self.widget("console-auth-remember").set_sensitive(True)
            if pw != "" or username != "":
                self.widget("console-auth-remember").set_active(True)
            else:
                self.widget("console-auth-remember").set_active(False)
        else:
            self.widget("console-auth-remember").set_sensitive(False)
        self.widget("console-pages").set_current_page(PAGE_AUTHENTICATE)
        if withUsername:
            self.widget("console-auth-username").grab_focus()
        else:
            self.widget("console-auth-password").grab_focus()


    def activate_viewer_page(self):
        self.widget("console-pages").set_current_page(PAGE_VIEWER)
        self.widget("details-menu-vm-screenshot").set_sensitive(True)
        if self.viewer and self.viewer.display:
            self.viewer.display.grab_focus()

    def page_changed(self, ignore1=None, ignore2=None, ignore3=None):
        self.set_allow_fullscreen()

    def set_allow_fullscreen(self):
        cpage = self.widget("console-pages").get_current_page()
        dpage = self.widget("details-pages").get_current_page()

        allow_fullscreen = (dpage == 0 and
                            cpage == PAGE_VIEWER and
                            self.viewer_connected)

        self.widget("control-fullscreen").set_sensitive(allow_fullscreen)
        self.widget("details-menu-view-fullscreen").set_sensitive(allow_fullscreen)

    def disconnected(self):
        errout = ""
        if self.tunnels is not None:
            errout = self.tunnels.get_err_output()
            self.tunnels.close_all()
            self.tunnels = None

        self.close_viewer()
        logging.debug("Viewer disconnected")

        # Make sure modifiers are set correctly
        self.viewer_focus_changed()

        if (self.skip_connect_attempt() or
            self.guest_not_avail()):
            # Exit was probably for legitimate reasons
            self.view_vm_status()
            return

        error = _("Error: viewer connection to hypervisor host got refused "
                  "or disconnected!")
        if errout:
            logging.debug("Error output from closed console: %s", errout)
            error += "\n\nError: %s" % errout

        self.activate_unavailable_page(error)

    def _set_viewer_connected(self, val):
        self._viewer_connected = val
        self.set_allow_fullscreen()
    def _get_viewer_connected(self):
        return self._viewer_connected
    viewer_connected = property(_get_viewer_connected, _set_viewer_connected)

    def connected(self):
        self.viewer_connected = True
        logging.debug("Viewer connected")
        self.activate_viewer_page()

        # Had a succesfull connect, so reset counters now
        self.viewerRetriesScheduled = 0
        self.viewerRetryDelay = 125

        # Make sure modifiers are set correctly
        self.viewer_focus_changed()

    def schedule_retry(self):
        if self.viewerRetriesScheduled >= 10:
            logging.error("Too many connection failures, not retrying again")
            return

        self.timeout_add(self.viewerRetryDelay, self.try_login)

        if self.viewerRetryDelay < 2000:
            self.viewerRetryDelay = self.viewerRetryDelay * 2

    def skip_connect_attempt(self):
        return (self.viewer or
                not self.is_visible())

    def guest_not_avail(self):
        return (self.vm.is_shutoff() or self.vm.is_crashed())

    def try_login(self, src_ignore=None):
        if self.viewer_connecting:
            return

        try:
            self.viewer_connecting = True
            self._try_login()
        finally:
            self.viewer_connecting = False

    def _try_login(self):
        if self.skip_connect_attempt():
            # Don't try and login for these cases
            return

        if self.guest_not_avail():
            # Guest isn't running, schedule another try
            self.activate_unavailable_page(_("Guest not running"))
            self.schedule_retry()
            return

        ginfo = None
        try:
            gdevs = self.vm.get_graphics_devices()
            gdev = gdevs and gdevs[0] or None
            if gdev:
                ginfo = ConnectionInfo(self.vm.conn, gdev)
        except Exception, e:
            # We can fail here if VM is destroyed: xen is a bit racy
            # and can't handle domain lookups that soon after
            logging.exception("Getting graphics console failed: %s", str(e))
            return

        if ginfo is None:
            logging.debug("No graphics configured for guest")
            self.activate_unavailable_page(
                            _("Graphical console not configured for guest"))
            return

        if ginfo.gtype not in self.config.embeddable_graphics():
            logging.debug("Don't know how to show graphics type '%s' "
                          "disabling console page", ginfo.gtype)

            msg = (_("Cannot display graphical console type '%s'")
                     % ginfo.gtype)

            self.activate_unavailable_page(msg)
            return

        if ginfo.console_active():
            self.activate_unavailable_page(
                            _("Graphical console is not yet active for guest"))
            self.schedule_retry()
            return

        self.activate_unavailable_page(
                _("Connecting to graphical console for guest"))

        logging.debug("Starting connect process for %s", ginfo.logstring())
        try:
            if ginfo.gtype == "vnc":
                self.viewer = VNCViewer(self)
                self.widget("console-gfx-viewport").add(self.viewer.display)
                self.viewer.init_widget()
            elif ginfo.gtype == "spice":
                self.viewer = SpiceViewer(self)

            self.set_enable_accel()

            if ginfo.need_tunnel():
                if self.tunnels:
                    # Tunnel already open, no need to continue
                    return

                self.tunnels = Tunnels(ginfo)
                self.viewer.open_fd(self.tunnels.open_new())
            else:
                self.viewer.open_host(ginfo)

        except Exception, e:
            logging.exception("Error connection to graphical console")
            self.activate_unavailable_page(
                    _("Error connecting to graphical console") + ":\n%s" % e)

    def set_credentials(self, src_ignore=None):
        passwd = self.widget("console-auth-password")
        if passwd.get_visible():
            self.viewer.set_credential_password(passwd.get_text())
        username = self.widget("console-auth-username")
        if username.get_visible():
            self.viewer.set_credential_username(username.get_text())

        if self.widget("console-auth-remember").get_active():
            self.config.set_console_password(self.vm, passwd.get_text(),
                                             username.get_text())

    def queue_scroll_resize_helper(self, w, h):
        """
        Resize the VNC container widget to the requested size. The new size
        isn't a hard requirment so the user can still shrink the window
        again, as opposed to set_size_request
        """
        widget = self.widget("console-gfx-scroll")
        signal_holder = []

        def restore_scroll(src):
            is_scale = self.viewer.get_scaling()

            if is_scale:
                w_policy = Gtk.PolicyType.NEVER
                h_policy = Gtk.PolicyType.NEVER
            else:
                w_policy = Gtk.PolicyType.AUTOMATIC
                h_policy = Gtk.PolicyType.AUTOMATIC

            src.set_policy(w_policy, h_policy)
            return False

        def unset_cb(src):
            src.queue_resize_no_redraw()
            self.idle_add(restore_scroll, src)
            return False

        def request_cb(src, req):
            signal_id = signal_holder[0]
            req.width = w
            req.height = h

            src.disconnect(signal_id)

            self.idle_add(unset_cb, widget)
            return False

        # Disable scroll bars while we resize, since resizing to the VM's
        # dimensions can erroneously show scroll bars when they aren't needed
        widget.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)

        signal_id = widget.connect("size-allocate", request_cb)
        signal_holder.append(signal_id)

        widget.queue_resize()

    def scroll_size_allocate(self, src_ignore, req):
        if not self.viewer or not self.viewer.get_desktop_resolution():
            return

        scroll = self.widget("console-gfx-scroll")
        is_scale = self.viewer.get_scaling()

        dx = 0
        dy = 0
        align_ratio = float(req.width) / float(req.height)

        desktop_w, desktop_h = self.viewer.get_desktop_resolution()
        if desktop_h == 0:
            return
        desktop_ratio = float(desktop_w) / float(desktop_h)

        if not is_scale:
            # Scaling disabled is easy, just force the VNC widget size. Since
            # we are inside a scrollwindow, it shouldn't cause issues.
            scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
            self.viewer.display.set_size_request(desktop_w, desktop_h)
            return

        # Make sure we never show scrollbars when scaling
        scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)

        # Make sure there is no hard size requirement so we can scale down
        self.viewer.display.set_size_request(-1, -1)

        # Make sure desktop aspect ratio is maintained
        if align_ratio > desktop_ratio:
            desktop_w = int(req.height * desktop_ratio)
            desktop_h = req.height
            dx = (req.width - desktop_w) / 2

        else:
            desktop_w = req.width
            desktop_h = int(req.width / desktop_ratio)
            dy = (req.height - desktop_h) / 2

        viewer_alloc = Gdk.Rectangle()
        viewer_alloc.x = dx
        viewer_alloc.y = dy
        viewer_alloc.width = desktop_w
        viewer_alloc.height = desktop_h
        self.viewer.display.size_allocate(viewer_alloc)
