#!/usr/bin/python3

## Copyright (C) 2008, 2009, 2010, 2012, 2014 Red Hat, Inc.
## Authors:
##  Tim Waugh <twaugh@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 dbus.mainloop.glib
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import Gtk
import subprocess
import threading
import config
import gettext
gettext.install(domain=config.PACKAGE, localedir=config.localedir)
from debug import *

# Initialise threading for D-Bus.  This is needed as long as it is
# used from two separate threads.  We only do this in a few places
# now, but in particular the troubleshooter does this (bug #662047).
Gdk.threads_init ()
dbus.mainloop.glib.threads_init ()

class OperationCanceled(RuntimeError):
    pass

class Timed:
    def run (self):
        pass

    def cancel (self):
        return False

class TimedSubprocess(Timed):
    def __init__ (self, timeout=60000, parent=None, show_dialog=True,
                  **args):
        self.subp = subprocess.Popen (**args)
        self.output = dict()
        self.io_source = []
        self.watchers = 2
        self.timeout = timeout
        self.parent = parent
        self.show_dialog = show_dialog
        for f in [self.subp.stdout, self.subp.stderr]:
            if f is not None:
                source = GLib.io_add_watch (f,
                                            GLib.PRIORITY_DEFAULT,
                                            GLib.IO_IN |
                                            GLib.IO_HUP |
                                            GLib.IO_ERR,
                                            self.watcher)
                self.io_source.append (source)

        self.wait_window = None

    def run (self):
        if self.show_dialog:
            self.wait_source = GLib.timeout_add_seconds (
                1,
                self.show_wait_window)

        self.timeout_source = GLib.timeout_add (self.timeout,
                                                self.do_timeout)
        Gtk.main ()
        if self.timeout_source:
            GLib.source_remove (self.timeout_source)
        if self.show_dialog:
            GLib.source_remove (self.wait_source)
        for source in self.io_source:
            GLib.source_remove (source)
        if self.wait_window is not None:
            self.wait_window.destroy ()
        return (self.output.get (self.subp.stdout, '').split ('\n'),
                self.output.get (self.subp.stderr, '').split ('\n'),
                self.subp.poll ())

    def do_timeout (self):
        self.timeout_source = None
        Gtk.main_quit ()
        return False

    def watcher (self, source, condition):
        if condition & GLib.IO_IN:
            buffer = self.output.get (source, '')
            buffer += (source.read ()).decode("utf-8")
            self.output[source] = buffer

        if condition & GLib.IO_HUP:
            self.watchers -= 1
            if self.watchers == 0:
                Gtk.main_quit ()

        return True

    def show_wait_window (self):
        Gdk.threads_enter ()
        wait = Gtk.MessageDialog (parent=self.parent,
                                  modal=True, destroy_with_parent=True,
                                  message_type=Gtk.MessageType.INFO,
                                  buttons=Gtk.ButtonsType.CANCEL,
                                  text=_("Please wait"))
        wait.connect ("delete_event", lambda *args: False)
        wait.connect ("response", self.wait_window_response)
        if self.parent:
            wait.set_transient_for (self.parent)
        wait.set_position (Gtk.WindowPosition.CENTER_ON_PARENT)
        wait.format_secondary_text (_("Gathering information"))
        wait.show_all ()
        self.wait_window = wait
        Gdk.threads_leave ()
        return False

    def wait_window_response (self, dialog, response):
        if response == Gtk.ResponseType.CANCEL:
            self.cancel ()

    def cancel (self):
        if self.watchers > 0:
            debugprint ("Command canceled")
            Gtk.main_quit ()
            self.watchers = 0

        return False

class OperationThread(threading.Thread):
    def __init__ (self, target=None, args=(), kwargs={}):
        threading.Thread.__init__ (self)
        self.setDaemon (True)
        self.target = target
        self.args = args
        self.kwargs = kwargs
        self.exception = None
        self.result = None

    def run (self):
        try:
            debugprint ("Calling %s" % self.target)
            self.result = self.target (*self.args, **self.kwargs)
            debugprint ("Done")
        except Exception as e:
            debugprint ("Caught exception %s" % e)
            self.exception = e

    def collect_result (self):
        if self.is_alive ():
            # We've been canceled.
            raise OperationCanceled()

        if self.exception:
            raise self.exception

        return self.result

class TimedOperation(Timed):
    def __init__ (self, target, args=(), kwargs={}, parent=None,
                  show_dialog=False, callback=None, context=None):
        self.wait_window = None
        self.parent = parent
        self.show_dialog = show_dialog
        self.callback = callback
        self.context = context
        self.thread = OperationThread (target=target,
                                       args=args,
                                       kwargs=kwargs)
        self.thread.start ()

        self.use_callback = callback is not None
        if self.use_callback:
            self.timeout_source = GLib.timeout_add (50, self._check_thread)

    def run (self):
        if self.use_callback:
            raise RuntimeError

        if self.show_dialog:
            wait = Gtk.MessageDialog (parent=self.parent,
                                      modal=True, destroy_with_parent=True,
                                      message_type=Gtk.MessageType.INFO,
                                      buttons=Gtk.ButtonsType.CANCEL,
                                      text=_("Please wait"))
            wait.connect ("delete_event", lambda *args: False)
            wait.connect ("response", self._wait_window_response)
            if self.parent:
                wait.set_transient_for (self.parent)

            wait.set_position (Gtk.WindowPosition.CENTER_ON_PARENT)
            wait.format_secondary_text (_("Gathering information"))
            wait.show_all ()

        self.timeout_source = GLib.timeout_add (50, self._check_thread)
        Gtk.main ()
        if self.timeout_source:
            GLib.source_remove (self.timeout_source)
        if self.show_dialog:
            wait.destroy ()

        return self.thread.collect_result ()

    def _check_thread (self):
        if self.thread.is_alive ():
            # Thread still running.
            return True

        # Thread has finished.  Stop the sub-loop or trigger callback.
        self.timeout_source = False
        if self.use_callback:
            if self.callback is not None:
                if self.context is not None:
                    self.callback (self.thread.result, self.thread.exception,
                                   self.context)
                else:
                    self.callback (self.thread.result, self.thread.exception)
        else:
            Gtk.main_quit ()

        return False

    def _wait_window_response (self, dialog, response):
        if response == Gtk.ResponseType.CANCEL:
            self.cancel ()

    def cancel (self):
        debugprint ("Command canceled")
        if self.use_callback:
            self.callback = None
        else:
            Gtk.main_quit ()

        return False
