# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

"""Des fonctions pour lancer et gérer des processus externes
"""

__all__ = ('is_trace', 'is_stop_on_errors', 'spawn', 'spawnall', 'spawnone',
           'LinePumper', 'DataPumper', 'spawn_capture', 'spawn_redirect')

import os, sys, re
from select import select
from threading import Thread
import subprocess

from .base import isstr
from .uio import _s
from .control import Status, OK_STATUS

def __spawnlp(mode, cmd, *args):
    return subprocess.call([cmd] + list(args))

def __spawn_capture(capture_out, copy_err_on_out, capture_err, cmd, *args):
    cmd = (cmd,) + args
    stdout = subprocess.PIPE
    if capture_out and copy_err_on_out: stderr = subprocess.STDOUT
    else: stderr = subprocess.PIPE
    proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr, close_fds=True)
    stdout, stderr = proc.communicate()
    if not capture_out: stdout = None
    if not capture_err: stderr = None
    return Status(exitcode=proc.returncode), stdout, stderr

def __spawn_redirect(inf, outf, errf, copy_errf_on_outf, cmd, *args):
    cmd = (cmd,) + args
    if copy_errf_on_outf: errf = subprocess.STDOUT
    proc = subprocess.Popen(cmd, stdin=inf, stdout=outf, stderr=errf)
    proc.communicate()
    return proc.returncode

TRACE = 'trace'
STOP_ON_ERRORS = 'stop_on_errors'
STDIN = 'stdin'
STDOUT = 'stdout'
STDERR = 'stderr'

def is_trace(kw):
    """Indiquer si spawn doit afficher le nom de la commande qui est lancée.
    """
    return kw.get(TRACE, False)
def is_stop_on_errors(kw):
    """Indiquer si spawnall doit s'arrêter en cas d'erreur.
    """
    return kw.get(STOP_ON_ERRORS, True)

RE_ESCAPE = re.compile(r"\s|'")
def __quote(s):
    if RE_ESCAPE.search(s) is not None:
        s = s.replace("'", "'\''")
        return "'%s'" % s
    return s

def spawn(cmd, *args, **kw):
    """Lancer la commande cmd avec les arguments args, et retourner son status
    d'exécution. La commande est cherchée dans le PATH si nécessaire.

    @rtype: Status
    @raise OSError: Si la commande n'est pas trouvée dans le PATH.
    """
    if is_trace(kw):
        cmdline = "$ %s" % cmd
        for arg in args:
            cmdline += " %s" % __quote(arg)
        print cmdline
    exitcode = __spawnlp(os.P_WAIT, cmd, *args)
    if exitcode == 127:
        raise OSError(exitcode, "Command not found: %s" % _s(cmd))
    return Status(exitcode)

def spawnall(*argss, **kw):
    """Lancer toutes les commandes de argss, en s'arrêtant à la première erreur.

    Si kw[STOP_ON_ERRORS] == False, on lance toutes les commande sans se
    préoccuper de leur code de retour.
    Retourner le status d'exécution de la dernière commande lancée.

    @rtype: Status
    """
    stop_on_errors = is_stop_on_errors(kw)
    status = OK_STATUS
    for args in argss:
        status = spawn(*args, **kw)
        if not status and stop_on_errors: break
    return status

def spawnone(*argss, **kw):
    """Lancer toutes les commandes de argss, en s'arrêtant à la première
    commande qui s'exécute sans erreur.

    @rtype: Status
    """
    status = OK_STATUS
    for args in argss:
        status = spawn(*args, **kw)
        if status: break
    return status

class Pumper(Thread):
    """Un thread qui lit les données sur un flux et l'écrit sur un autre.

    En principe, outf est un objet fichier.
    Si outf est None, alors les méthodes write_data, flush_outf et close_outf
    doivent être surchargées pour implémenter la nouvelle stratégie d'écriture.
    """
    def __init__(self, inf, outf,
                 stop_on_eof=True, close_on_eof=True,
                 flush_on_write=True,
                 start=True):
        Thread.__init__(self)
        self.setDaemon(True)
        self.pumping = True
        self.inf = inf
        self.outf = outf
        self.stop_on_eof = stop_on_eof
        self.close_on_eof = close_on_eof
        self.flush_on_write = flush_on_write
        if start: self.start()

    def read_data(self):
        raise NotImplementedError
    def write_data(self, data):
        self.outf.write(data)
    def flush_outf(self):
        self.outf.flush()
    def close_outf(self):
        self.outf.close()

    def run(self):
        while self.pumping:
            ins = [self.inf]
            if self.outf is None: outs = []
            else: [self.outf]
            inr, outr, _ = select(ins, outs, [], 0.1)
            if self.inf in inr and (not outs or self.outf in outr):
                data = self.read_data()
                if data:
                    self.write_data(data)
                    if self.flush_on_write: self.flush_outf()
                elif self.stop_on_eof:
                    if self.close_on_eof: self.close_outf()
                    break

    def stop(self):
        """Arrêter le pompage dès que possible.
        """
        self.pumping = False
        self.join()

class LinePumper(Pumper):
    """Un pumper qui lit les données ligne par ligne
    """
    def read_data(self):
        return self.inf.readline()

class DataPumper(Pumper):
    """Un pumper qui lit les données par quantité de bufsize.
    """
    bufsize = 4096

    def set_bufsize(self, bufsize):
        self.bufsize = bufsize

    def read_data(self):
        return self.inf.read(self.bufsize)

class StringPumperMixin:
    """Un mixin à utiliser avec les classes dérivées de Pumper pour écrire
    dans une chaine plutôt que sur un flux.
    """
    buffer = None

    def write_data(self, data):
        buffer = self.buffer
        if buffer is None: buffer = ""
        buffer += data
        self.buffer = buffer
    def flush_outf(self): pass
    def close_outf(self): pass

class StringLinePumper(StringPumperMixin, LinePumper):
    pass

class StringDataPumper(StringPumperMixin, DataPumper):
    pass

def spawn_capture(cmd, *args, **kw):
    """Lancer la commande cmd avec les arguments args, et retourner son status
    d'exécution, ainsi que sa sortie standard et (éventuellement) sa sortie
    d'erreur sous forme de chaine.

    Si kw ne contient pas la clé stdout avec une valeur fausse, la sortie
    standard sera capturée. Si kw contient la clé stderr avec une valeur vraie,
    la sortie d'erreur sera capturée. Si kw contient la clé stderr avec une
    valeur fausse, la sortie d'erreur ne sera pas capturée. Sinon, la sortie
    d'erreur sera connectée sur la sortie standard.

    @rtype: tuple
    @return: (status, stdout, stderr)
    """
    capture_out = kw.get(STDOUT, True) and True or False
    capture_err = kw.get(STDERR, None)
    if capture_err is None:
        copy_err_on_out = True
    else:
        copy_err_on_out = False
        capture_err = capture_err and True or False
    return __spawn_capture(capture_out, copy_err_on_out, capture_err, cmd, *args)

def spawn_redirect(cmd, *args, **kw):
    """Lancer la commande cmd avec les arguments args, et retourner son status
    d'exécution.

    Si kw contient la clé stdin, l'entrée standard de cmd sera connectée sur
    ce fichier. Si kw contient la clé stdout, la sortie standard de cmd sera
    connectée sur ce fichier. Si kw ne contient pas la clé stderr, la sortie
    d'erreur de cmd sera connectée sur stdout si défini.

    Les valeurs stdout et stderr qui sont retournées sont les valeurs prises
    de kw telles quelles.

    @rtype: tuple
    @return: (status, stdout, stderr)
    """
    inf = stdin = kw.get(STDIN, None); close_inf = False
    outf = stdout = kw.get(STDOUT, None); close_outf = False
    errf = stderr = kw.get(STDERR, None); close_errf = False
    if stdin is None and stdout is None and stderr is None:
        return spawn(cmd, *args, **kw), None, None

    if inf is None:
        inf = sys.stdin
    elif isstr(inf):
        inf = open(inf, 'rb')
        close_inf = True
    if outf is None:
        outf = sys.stdout
    elif isstr(outf):
        outf = open(outf, 'wb')
        close_outf = True
    copy_errf_on_outf = errf is None
    if isstr(errf):
        errf = open(errf, 'wb')
        close_errf = True

    if is_trace(kw):
        cmdline = "$ %s" % cmd
        for arg in args:
            cmdline += " %s" % __quote(arg)
        print cmdline

    try:
        exitcode = __spawn_redirect(inf, outf, errf, copy_errf_on_outf, cmd, *args)
        return Status(exitcode=exitcode), stdout, stderr
    finally:
        if close_errf: errf.close()
        if close_outf: outf.close()
        if close_inf: inf.close()
