# cmdcore.py - run Mercurial commands in a separate thread or process
#
# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

import os, sys, time

from PyQt4.QtCore import QIODevice, QObject, QProcess
from PyQt4.QtCore import pyqtSignal, pyqtSlot

from tortoisehg.util import hglib, paths
from tortoisehg.hgqt.i18n import _
from tortoisehg.hgqt import thread

def _findhgexe():
    exepath = None
    if hasattr(sys, 'frozen'):
        progdir = paths.get_prog_root()
        exe = os.path.join(progdir, 'hg.exe')
        if os.path.exists(exe):
            exepath = exe
    if not exepath:
        exepath = paths.find_in_path('hg')
    return exepath

class CmdProc(QObject):
    'Run mercurial command in separate process'

    started = pyqtSignal()
    commandFinished = pyqtSignal(int)
    outputReceived = pyqtSignal(unicode, unicode)

    # progress is not supported but needed to be a worker class
    progressReceived = pyqtSignal(unicode, object, unicode, unicode, object)

    def __init__(self, cmdline, parent=None):
        super(CmdProc, self).__init__(parent)
        self.cmdline = cmdline

        self._proc = proc = QProcess(self)
        proc.started.connect(self.started)
        proc.finished.connect(self.commandFinished)
        proc.readyReadStandardOutput.connect(self._stdout)
        proc.readyReadStandardError.connect(self._stderr)
        proc.error.connect(self._handleerror)

    def start(self):
        self._proc.start(_findhgexe(), self.cmdline, QIODevice.ReadOnly)

    def abort(self):
        if not self.isRunning():
            return
        self._proc.close()

    def isRunning(self):
        return self._proc.state() != QProcess.NotRunning

    def _handleerror(self, error):
        if error == QProcess.FailedToStart:
            self.outputReceived.emit(_('failed to start command\n'),
                                     'ui.error')
            self.commandFinished.emit(-1)
        elif error != QProcess.Crashed:
            self.outputReceived.emit(_('error while running command\n'),
                                     'ui.error')

    def _stdout(self):
        data = self._proc.readAllStandardOutput().data()
        self.outputReceived.emit(hglib.tounicode(data), '')

    def _stderr(self):
        data = self._proc.readAllStandardError().data()
        self.outputReceived.emit(hglib.tounicode(data), 'ui.error')


def _quotecmdarg(arg):
    # only for display; no use to construct command string for os.system()
    if not arg or ' ' in arg or '\\' in arg or '"' in arg:
        return '"%s"' % arg.replace('"', '\\"')
    else:
        return arg

def _prettifycmdline(cmdline):
    r"""Build pretty command-line string for display

    >>> _prettifycmdline(['--repository', 'foo', 'status'])
    'status'
    >>> _prettifycmdline(['--cwd', 'foo', 'resolve', '--', '--repository'])
    'resolve -- --repository'
    >>> _prettifycmdline(['log', 'foo\\bar', '', 'foo bar', 'foo"bar'])
    'log "foo\\bar" "" "foo bar" "foo\\"bar"'
    """
    try:
        argcount = cmdline.index('--')
    except ValueError:
        argcount = len(cmdline)
    printables = []
    pos = 0
    while pos < argcount:
        if cmdline[pos] in ('-R', '--repository', '--cwd'):
            pos += 2
        else:
            printables.append(cmdline[pos])
            pos += 1
    printables.extend(cmdline[argcount:])

    return ' '.join(_quotecmdarg(e) for e in printables)

class CmdSession(QObject):
    """Run Mercurial commands in a background thread or process"""

    commandStarted = pyqtSignal()
    commandFinished = pyqtSignal(int)

    outputReceived = pyqtSignal(unicode, unicode)
    progressReceived = pyqtSignal(unicode, object, unicode, unicode, object)

    def __init__(self, parent=None):
        super(CmdSession, self).__init__(parent)

        self._worker = None
        self.queue = []
        self.display = None
        self.useproc = False
        self._abortbyuser = False

    ### Public Methods ###

    def run(self, cmdlines, display=None, useproc=False):
        '''Execute or queue Mercurial command'''
        self.display = display
        self.useproc = useproc
        self.queue.extend(cmdlines)
        if not self.running():
            self.runNext()

    def cancel(self):
        '''Cancel running Mercurial command'''
        if self.running():
            self._worker.abort()
            self._abortbyuser = True

    def running(self):
        # keep "running" until just before emitting commandFinished. if worker
        # is QThread, isRunning() is cleared earlier than onCommandFinished,
        # because inter-thread signal is queued.
        return bool(self._worker)

    ### Private Method ###

    def _createWorker(self, cmdline):
        cmdline = map(hglib.fromunicode, cmdline)
        if self.useproc:
            return CmdProc(cmdline, self)
        else:
            return thread.CmdThread(cmdline, self)

    def runNext(self):
        if not self.queue:
            return False

        cmdline = self.queue.pop(0)

        if not self.display:
            self.display = _prettifycmdline(cmdline)
        self._worker = self._createWorker(cmdline)
        self._worker.started.connect(self.onCommandStarted)
        self._worker.commandFinished.connect(self.onCommandFinished)

        self._worker.outputReceived.connect(self.outputReceived)
        self._worker.progressReceived.connect(self.progressReceived)

        self._abortbyuser = False
        self._worker.start()
        return True

    ### Signal Handlers ###

    @pyqtSlot()
    def onCommandStarted(self):
        self.commandStarted.emit()
        cmd = '%% hg %s\n' % self.display
        self.outputReceived.emit(cmd, 'control')

    @pyqtSlot(int)
    def onCommandFinished(self, ret):
        if ret == -1:
            if self._abortbyuser:
                msg = _('[command terminated by user %s]')
            else:
                msg = _('[command interrupted %s]')
        elif ret:
            msg = _('[command returned code %d %%s]') % ret
        else:
            msg = _('[command completed successfully %s]')
        self.outputReceived.emit(msg % time.asctime() + '\n', 'control')

        self.display = None
        self._worker.setParent(None)  # assist gc
        if ret == 0 and self.runNext():
            return # run next command
        else:
            self.queue = []
            self._worker = None

        self.commandFinished.emit(ret)


class CmdAgent(QObject):
    """Manage requests of Mercurial commands"""

    busyChanged = pyqtSignal(bool)
    outputReceived = pyqtSignal(unicode, unicode)
    progressReceived = pyqtSignal(unicode, object, unicode, unicode, object)

    def __init__(self, parent=None):
        super(CmdAgent, self).__init__(parent)
        self._cwd = None
        self._busycount = 0

    def workingDirectory(self):
        return self._cwd

    def setWorkingDirectory(self, cwd):
        self._cwd = unicode(cwd)

    def isBusy(self):
        return self._busycount > 0

    def _incrementBusyCount(self):
        self._busycount += 1
        if self._busycount == 1:
            self.busyChanged.emit(self.isBusy())

    def _decrementBusyCount(self):
        self._busycount -= 1
        if self._busycount == 0:
            self.busyChanged.emit(self.isBusy())

    def runCommand(self, cmdline, parent=None, display=None, worker=None):
        """Executes a single Mercurial command asynchronously and returns
        new CmdSession object"""
        return self.runCommandSequence([cmdline], parent=parent,
                                       display=display, worker=worker)

    def runCommandSequence(self, cmdlines, parent=None, display=None,
                           worker=None):
        """Executes a series of Mercurial commands asynchronously and returns
        new CmdSession object which will provide notification signals.

        CmdSession object will be disowned on command finished, even if parent
        is specified.

        If one of the preceding command exits with non-zero status, the
        following commands won't be executed.
        """
        if self._cwd:
            # TODO: pass to CmdSession so that CmdProc can use it directly
            cmdlines = [['--cwd', self._cwd] + list(xs) for xs in cmdlines]
        useproc = worker == 'proc'
        if not parent:
            parent = self
        # TODO: queue multiple requests or just disable while busy?
        sess = CmdSession(parent)
        sess.commandFinished.connect(self._onCommandFinished)
        sess.outputReceived.connect(self.outputReceived)
        sess.progressReceived.connect(self.progressReceived)

        # It's safe to connect commandFinished, outputReceived and
        # progressReceived soon after run(), because inter-thread signals
        # are queued.  But it doesn't apply to commandStarted.
        # TODO: delay sess.run by QTimer to make sure no signal emitted in
        # the current context?
        self._incrementBusyCount()
        sess.run(cmdlines, display=display, useproc=useproc)
        return sess

    #@pyqtSlot()
    def _onCommandFinished(self):
        sess = self.sender()
        assert isinstance(sess, CmdSession)
        sess.setParent(None)
        self._decrementBusyCount()
