# Copyright (C) 2009 Canonical Ltd
#
# 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 St, Fifth Floor, Boston, MA 02110-1301  USA

import os, sys
import subprocess

from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import QProcess

from bzrlib.plugins.explorer.lib.i18n import gettext, N_


# Explanations of why a process failed
_PROCESS_ERRORS = {
    QProcess.FailedToStart:     gettext("failed to start"),
    QProcess.Crashed:           gettext("process crashed"),
    QProcess.Timedout:          gettext("process timed out"),
    QProcess.WriteError:        gettext("write error"),
    QProcess.ReadError:         gettext("read error"),
    QProcess.UnknownError:      gettext("unknown error"),
    }
_UNKNOWN_ERROR = gettext("unknown error")


class ApplicationRunner(QtCore.QObject):
    """An object that runs an external application.

    Responsibilities include:

    * trapping and reporting of launch errors

    * clean-up of completed processes.
    """

    def __init__(self, error_reporter):
        """Create an application runner.

        :param error_reporter: a callable that display an error. The
          required parameters are (title, msg).
        """
        self._error_reporter = error_reporter
        # We need to retain references to QProcesses to stop them being
        # garbage collected too early
        self._child_processes = {}

    def _report_error(self, process_error):
        reason = _PROCESS_ERRORS.get(process_error, _UNKNOWN_ERROR)
        msg = gettext("Process error: %s") % (reason,)
        self._error_reporter("Error", msg)

    def run_app(self, args, success_callback=None, failure_callback=None,
                use_shell=False):
        """Run an external application.

        :param args: list of arguments, first one is the command itself.
        :param success_callback: if not None, a callable to invoke when
          the exit code is zero.
        :param failure_callback: if not None, a callable to invoke when
          the exit code is not zero.
        :param use_shell: if True, cause the subprocess to use a shell
          to invoke args.
        """
        # See lp:401579 for details on why this is needed
        if sys.platform == 'win32':
            args = [arg.encode('mbcs') for arg in args]

        if use_shell:
            # stitch back together:  shell will tokenize for itself.
            args = u' '.join(args)

        # If there are no callbacks, use the well tested subprocess module
        if success_callback is None and failure_callback is None:
            subprocess.Popen(args, shell=use_shell)
            return

        # Use Qt's fancy process management  stuff, including signals
        # XXX: should we capture stderr and display it on completion as well?
        def do_finished(exit_code, exit_status):
            self._kill_zombies()
            if exit_status == QProcess.CrashExit:
                self._report_error(QProcess.Crashed)
            elif exit_code == 0 and success_callback is not None:
                success_callback()
            elif exit_code != 0 and failure_callback is not None:
                failure_callback()

        app_process = QtCore.QProcess()
        self._child_processes[app_process] = args
        self.connect(app_process, QtCore.SIGNAL("error(int)"),
            self._report_error)
        self.connect(app_process,
            QtCore.SIGNAL("finished(int,QProcess::ExitStatus)"),
            do_finished)
        if len(args) == 1:
            app_process.start(args[0])
        else:
            app_process.start(args[0], args[1:])

    def _kill_zombies(self):
        """Garbage collect processes no longer running."""
        children = list(self._child_processes.keys())
        for child in children:
            if child.state() == QProcess.NotRunning:
                del self._child_processes[child]
