import errno
import sys
from types import FunctionType

import eventlet
from eventlet import greenio
from eventlet import patcher
from eventlet.green import select, threading, time
import six


__patched__ = ['call', 'check_call', 'Popen']
to_patch = [('select', select), ('threading', threading), ('time', time)]

if sys.version_info > (3, 4):
    from eventlet.green import selectors
    to_patch.append(('selectors', selectors))

patcher.inject('subprocess', globals(), *to_patch)
subprocess_orig = patcher.original("subprocess")
subprocess_imported = sys.modules.get('subprocess', subprocess_orig)
mswindows = sys.platform == "win32"


if getattr(subprocess_orig, 'TimeoutExpired', None) is None:
    # Backported from Python 3.3.
    # https://bitbucket.org/eventlet/eventlet/issue/89
    class TimeoutExpired(Exception):
        """This exception is raised when the timeout expires while waiting for
        a child process.
        """

        def __init__(self, cmd, timeout, output=None):
            self.cmd = cmd
            self.timeout = timeout
            self.output = output

        def __str__(self):
            return ("Command '%s' timed out after %s seconds" %
                    (self.cmd, self.timeout))
else:
    TimeoutExpired = subprocess_imported.TimeoutExpired


# This is the meat of this module, the green version of Popen.
class Popen(subprocess_orig.Popen):
    """eventlet-friendly version of subprocess.Popen"""
    # We do not believe that Windows pipes support non-blocking I/O. At least,
    # the Python file objects stored on our base-class object have no
    # setblocking() method, and the Python fcntl module doesn't exist on
    # Windows. (see eventlet.greenio.set_nonblocking()) As the sole purpose of
    # this __init__() override is to wrap the pipes for eventlet-friendly
    # non-blocking I/O, don't even bother overriding it on Windows.
    if not mswindows:
        def __init__(self, args, bufsize=0, *argss, **kwds):
            self.args = args
            # Forward the call to base-class constructor
            subprocess_orig.Popen.__init__(self, args, 0, *argss, **kwds)
            # Now wrap the pipes, if any. This logic is loosely borrowed from
            # eventlet.processes.Process.run() method.
            for attr in "stdin", "stdout", "stderr":
                pipe = getattr(self, attr)
                if pipe is not None and type(pipe) != greenio.GreenPipe:
                    # https://github.com/eventlet/eventlet/issues/243
                    # AttributeError: '_io.TextIOWrapper' object has no attribute 'mode'
                    mode = getattr(pipe, 'mode', '')
                    if not mode:
                        if pipe.readable():
                            mode += 'r'
                        if pipe.writable():
                            mode += 'w'
                        # ValueError: can't have unbuffered text I/O
                        if bufsize == 0:
                            bufsize = -1
                    wrapped_pipe = greenio.GreenPipe(pipe, mode, bufsize)
                    setattr(self, attr, wrapped_pipe)
        __init__.__doc__ = subprocess_orig.Popen.__init__.__doc__

    def wait(self, timeout=None, check_interval=0.01):
        # Instead of a blocking OS call, this version of wait() uses logic
        # borrowed from the eventlet 0.2 processes.Process.wait() method.
        if timeout is not None:
            endtime = time.time() + timeout
        try:
            while True:
                status = self.poll()
                if status is not None:
                    return status
                if timeout is not None and time.time() > endtime:
                    raise TimeoutExpired(self.args, timeout)
                eventlet.sleep(check_interval)
        except OSError as e:
            if e.errno == errno.ECHILD:
                # no child process, this happens if the child process
                # already died and has been cleaned up
                return -1
            else:
                raise
    wait.__doc__ = subprocess_orig.Popen.wait.__doc__

    if not mswindows:
        # don't want to rewrite the original _communicate() method, we
        # just want a version that uses eventlet.green.select.select()
        # instead of select.select().
        _communicate = FunctionType(
            six.get_function_code(six.get_unbound_function(
                subprocess_orig.Popen._communicate)),
            globals())
        try:
            _communicate_with_select = FunctionType(
                six.get_function_code(six.get_unbound_function(
                    subprocess_orig.Popen._communicate_with_select)),
                globals())
            _communicate_with_poll = FunctionType(
                six.get_function_code(six.get_unbound_function(
                    subprocess_orig.Popen._communicate_with_poll)),
                globals())
        except AttributeError:
            pass


# Borrow subprocess.call() and check_call(), but patch them so they reference
# OUR Popen class rather than subprocess.Popen.
def patched_function(function):
    new_function = FunctionType(six.get_function_code(function), globals())
    if six.PY3:
        new_function.__kwdefaults__ = function.__kwdefaults__
    new_function.__defaults__ = function.__defaults__
    return new_function


call = patched_function(subprocess_orig.call)
check_call = patched_function(subprocess_orig.check_call)
# check_output is Python 2.7+
if hasattr(subprocess_orig, 'check_output'):
    __patched__.append('check_output')
    check_output = patched_function(subprocess_orig.check_output)
del patched_function

# Keep exceptions identity.
# https://github.com/eventlet/eventlet/issues/413
CalledProcessError = subprocess_imported.CalledProcessError
del subprocess_imported
