from __future__ import division, print_function

import sys
import os
import warnings
import abc
import functools
import itertools

from py import std
import psutil


# make map appear from the future
if sys.version_info < (3,):
    map = itertools.imap


class XProcessInfo:
    def __init__(self, path, name):
        self.name = name
        self.controldir = path.ensure(name, dir=1)
        self.logpath = self.controldir.join("xprocess.log")
        self.pidpath = self.controldir.join("xprocess.PID")
        self.pid = int(self.pidpath.read()) if self.pidpath.check() else None

    def terminate(self):
        # return codes:
        # 0   no work to do
        # 1   terminated
        # -1  failed to terminate

        if not self.pid or not self.isrunning():
            return 0

        timeout = 20

        try:
            proc = psutil.Process(self.pid)
            proc.terminate()
            try:
                proc.wait(timeout=timeout/2)
            except psutil.TimeoutExpired:
                proc.kill()
                proc.wait(timeout=timeout/2)
        except psutil.Error:
            return -1

        return 1

    def kill(self):
        warnings.warn("Use .terminate instead of .kill", DeprecationWarning, stacklevel=2)
        return self.terminate()

    def isrunning(self):
        if self.pid is None:
            return False
        try:
            proc = psutil.Process(self.pid)
        except psutil.NoSuchProcess:
            return False
        return proc.is_running()


class XProcess:
    def __init__(self, config, rootdir, log=None):
        self.config = config
        self.rootdir = rootdir
        class Log:
            def debug(self, msg, *args):
                print(msg % args)
        self.log = log or Log()

    def getinfo(self, name):
        """ return Process Info for the given external process. """
        return XProcessInfo(self.rootdir, name)

    def ensure(self, name, preparefunc, restart=False):
        """ returns (PID, logfile) from a newly started or already
            running process.

        @param name: name of the external process, used for caching info
                     across test runs.

        @param preparefunc:
                A subclass of ProcessStarter.

        @param restart: force restarting the process if it is running.

        @return: (PID, logfile) logfile will be seeked to the end if the
                 server was running, otherwise seeked to the line after
                 where the waitpattern matched.
        """
        from subprocess import Popen, STDOUT
        info = self.getinfo(name)
        if not restart and not info.isrunning():
            restart = True

        if restart:
            if info.pid is not None:
                info.terminate()
            controldir = info.controldir.ensure(dir=1)
            #controldir.remove()
            preparefunc = CompatStarter.wrap(preparefunc)
            starter = preparefunc(controldir, self)
            args = [str(x) for x in starter.args]
            self.log.debug("%s$ %s", controldir, " ".join(args))
            stdout = open(str(info.logpath), "wb", 0)
            kwargs = {'env': starter.env}
            if sys.platform == "win32":
                kwargs["startupinfo"] = sinfo = std.subprocess.STARTUPINFO()
                if sys.version_info >= (2,7):
                    sinfo.dwFlags |= std.subprocess.STARTF_USESHOWWINDOW
                    sinfo.wShowWindow |= std.subprocess.SW_HIDE
            else:
                kwargs["close_fds"] = True
                kwargs["preexec_fn"] = os.setpgrp  # no CONTROL-C
            popen = Popen(args, cwd=str(controldir),
                          stdout=stdout, stderr=STDOUT,
                          **kwargs)
            info.pid = pid = popen.pid
            info.pidpath.write(str(pid))
            self.log.debug("process %r started pid=%s", name, pid)
            stdout.close()
        f = info.logpath.open()
        if not restart:
            f.seek(0, 2)
        else:
            if not starter.wait(f):
                raise RuntimeError("Could not start process %s" % name)
            self.log.debug("%s process startup detected", name)
        logfiles = self.config.__dict__.setdefault("_extlogfiles", {})
        logfiles[name] = f
        self.getinfo(name)
        return info.pid, info.logpath

    def _infos(self):
        return (
            self.getinfo(p.basename)
            for p in self.rootdir.listdir()
        )

    def _xkill(self, tw):
        ret = 0
        for info in self._infos():
            termret = info.terminate()
            ret = ret or (termret==1)
            status = {
                1: 'TERMINATED',
                -1: 'FAILED TO TERMINATE',
                0: 'NO PROCESS FOUND',
            }[termret]
            tmpl = '{info.pid} {info.name} {status}'
            tw.line(tmpl.format(**locals()))
        return ret

    def _xshow(self, tw):
        for info in self._infos():
            running = 'LIVE' if info.isrunning() else 'DEAD'
            tmpl = '{info.pid} {info.name} {running} {info.logpath}'
            tw.line(tmpl.format(**locals()))
        return 0


class ProcessStarter(object):
    """
    Describes the characteristics of a process to start, waiting
    for a process to achieve a started state.
    """

    env = None
    """
    The environment in which to invoke the process.
    """

    def __init__(self, control_dir, process):
        self.control_dir = control_dir
        self.process = process

    @abc.abstractproperty
    def args(self):
        "The args to start the process"

    @abc.abstractproperty
    def pattern(self):
        "The pattern to match when the process has started"

    def wait(self, log_file):
        "Wait until the process is ready."
        lines = map(self.log_line, self.filter_lines(self.get_lines(log_file)))
        return any(
            std.re.search(self.pattern, line)
            for line in lines
        )

    def filter_lines(self, lines):
        # only consider the first non-empty 50 lines
        non_empty_lines = (x for x in lines if x.strip())
        return itertools.islice(non_empty_lines, 50)

    def log_line(self, line):
        self.process.log.debug(line)
        return line

    def get_lines(self, log_file):
        while True:
            line = log_file.readline()
            if not line:
                std.time.sleep(0.1)
            yield line


class CompatStarter(ProcessStarter):
    """
    A compatibility ProcessStarter to handle legacy preparefunc
    and warn of the deprecation.
    """

    # Define properties to satisfy the abstract property, though
    # they will be overridden at the instance.
    pattern = None
    args = None

    def __init__(self, preparefunc, control_dir, process):
        self.prep(*preparefunc(control_dir))
        super(CompatStarter, self).__init__(control_dir, process)

    def prep(self, wait, args, env=None):
        """
        Given the return value of a preparefunc, prepare this
        CompatStarter.
        """
        self.pattern = wait
        self.env = env
        self.args = args

        # wait is a function, supersedes the default behavior
        if callable(wait):
            self.wait = lambda lines: wait()

    @classmethod
    def wrap(self, starter_cls):
        """
        If starter_cls is not a ProcessStarter, assume it's the legacy
        preparefunc and return it bound to a CompatStarter.
        """
        if isinstance(starter_cls, type) and issubclass(starter_cls, ProcessStarter):
            return starter_cls
        depr_msg = 'Pass a ProcessStarter for preparefunc'
        warnings.warn(depr_msg, DeprecationWarning, stacklevel=3)
        return functools.partial(CompatStarter, starter_cls)