import errno
import os
import pty
import selectors
import subprocess

MAX_ERROR_OUTPUT_BYTES = 8192


def _read_chunk(fd, treat_eio_as_eof=False):
    try:
        return os.read(fd, 4096)
    except OSError as exc:
        if treat_eio_as_eof and exc.errno == errno.EIO:
            return b""
        raise


def _stream_output(fd, process, reporter, treat_eio_as_eof=False):
    selector = selectors.DefaultSelector()
    recent_output = bytearray()

    try:
        selector.register(fd, selectors.EVENT_READ)

        while selector.get_map():
            for key, _ in selector.select():
                data = _read_chunk(key.fileobj, treat_eio_as_eof=treat_eio_as_eof)
                if not data:
                    selector.unregister(key.fileobj)
                    os.close(key.fileobj)
                    continue

                reporter._clear()
                os.write(1, data)
                reporter._render()
                recent_output.extend(data)
                if len(recent_output) > MAX_ERROR_OUTPUT_BYTES:
                    del recent_output[:-MAX_ERROR_OUTPUT_BYTES]
    finally:
        selector.close()

    return_code = process.wait()
    if return_code != 0:
        raise subprocess.CalledProcessError(return_code, process.args, output=bytes(recent_output))


def run_command_with_reporter(cmd, cwd=None, reporter=None):
    if reporter is None:
        subprocess.run(cmd, cwd=cwd, check=True)
        return

    try:
        master_fd, slave_fd = pty.openpty()
    except OSError:
        process = subprocess.Popen(
            cmd,
            cwd=cwd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
        assert process.stdout is not None
        _stream_output(process.stdout.fileno(), process, reporter)
        return

    try:
        process = subprocess.Popen(
            cmd,
            cwd=cwd,
            stdout=slave_fd,
            stderr=slave_fd,
        )
    finally:
        os.close(slave_fd)

    _stream_output(master_fd, process, reporter, treat_eio_as_eof=True)
