# -*- coding: utf-8 -*-
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
"""Miscellaneous utility functions
"""
import os
import sys
import gc
import errno
import select
import locale
import datetime
from pathlib import Path
from subprocess import Popen, STDOUT, PIPE
from .filemanip import canonicalize_env, read_stream

from .. import logging

iflogger = logging.getLogger("nipype.interface")


class Stream(object):
    """Function to capture stdout and stderr streams with timestamps

    stackoverflow.com/questions/4984549/merge-and-sync-stdout-and-stderr/5188359
    """

    def __init__(self, name, impl):
        self._name = name
        self._impl = impl
        self._buf = ""
        self._rows = []
        self._lastidx = 0
        self.default_encoding = locale.getdefaultlocale()[1] or "UTF-8"

    def fileno(self):
        "Pass-through for file descriptor."
        return self._impl.fileno()

    def read(self, drain=0):
        "Read from the file descriptor. If 'drain' set, read until EOF."
        while self._read(drain) is not None:
            if not drain:
                break

    def _read(self, drain):
        "Read from the file descriptor"
        fd = self.fileno()
        buf = os.read(fd, 4096).decode(self.default_encoding)
        if not buf and not self._buf:
            return None
        if "\n" not in buf:
            if not drain:
                self._buf += buf
                return []

        # prepend any data previously read, then split into lines and format
        buf = self._buf + buf
        if "\n" in buf:
            tmp, rest = buf.rsplit("\n", 1)
        else:
            tmp = buf
            rest = None
        self._buf = rest
        now = datetime.datetime.now().isoformat()
        rows = tmp.split("\n")
        self._rows += [(now, "%s %s:%s" % (self._name, now, r), r) for r in rows]
        for idx in range(self._lastidx, len(self._rows)):
            iflogger.info(self._rows[idx][1])
        self._lastidx = len(self._rows)


def run_command(runtime, output=None, timeout=0.01, write_cmdline=False):
    """Run a command, read stdout and stderr, prefix with timestamp.

    The returned runtime contains a merged stdout+stderr log with timestamps
    """

    # Init variables
    cmdline = runtime.cmdline
    env = canonicalize_env(runtime.environ)

    errfile = None
    outfile = None
    stdout = PIPE
    stderr = PIPE

    if output == "file":
        outfile = os.path.join(runtime.cwd, "output.nipype")
        stdout = open(outfile, "wb")  # t=='text'===default
        stderr = STDOUT
    elif output == "file_split":
        outfile = os.path.join(runtime.cwd, "stdout.nipype")
        stdout = open(outfile, "wb")
        errfile = os.path.join(runtime.cwd, "stderr.nipype")
        stderr = open(errfile, "wb")
    elif output == "file_stdout":
        outfile = os.path.join(runtime.cwd, "stdout.nipype")
        stdout = open(outfile, "wb")
    elif output == "file_stderr":
        errfile = os.path.join(runtime.cwd, "stderr.nipype")
        stderr = open(errfile, "wb")

    if write_cmdline:
        (Path(runtime.cwd) / "command.txt").write_text(cmdline)

    proc = Popen(
        cmdline,
        stdout=stdout,
        stderr=stderr,
        shell=True,
        cwd=runtime.cwd,
        env=env,
        close_fds=(not sys.platform.startswith("win")),
    )

    result = {
        "stdout": [],
        "stderr": [],
        "merged": [],
    }

    if output == "stream":
        streams = [Stream("stdout", proc.stdout), Stream("stderr", proc.stderr)]

        def _process(drain=0):
            try:
                res = select.select(streams, [], [], timeout)
            except select.error as e:
                iflogger.info(e)
                if e.errno == errno.EINTR:
                    return
                else:
                    raise
            else:
                for stream in res[0]:
                    stream.read(drain)

        while proc.returncode is None:
            proc.poll()
            _process()

        _process(drain=1)

        # collect results, merge and return
        result = {}
        temp = []
        for stream in streams:
            rows = stream._rows
            temp += rows
            result[stream._name] = [r[2] for r in rows]
        temp.sort()
        result["merged"] = [r[1] for r in temp]

    if output.startswith("file"):
        proc.wait()
        if outfile is not None:
            stdout.flush()
            stdout.close()
            with open(outfile, "rb") as ofh:
                stdoutstr = ofh.read()
            result["stdout"] = read_stream(stdoutstr, logger=iflogger)
            del stdoutstr

        if errfile is not None:
            stderr.flush()
            stderr.close()
            with open(errfile, "rb") as efh:
                stderrstr = efh.read()
            result["stderr"] = read_stream(stderrstr, logger=iflogger)
            del stderrstr

        if output == "file":
            result["merged"] = result["stdout"]
            result["stdout"] = []
    else:
        stdout, stderr = proc.communicate()
        if output == "allatonce":  # Discard stdout and stderr otherwise
            result["stdout"] = read_stream(stdout, logger=iflogger)
            result["stderr"] = read_stream(stderr, logger=iflogger)

    runtime.returncode = proc.returncode
    try:
        proc.terminate()  # Ensure we are done
    except OSError as error:
        # Python 2 raises when the process is already gone
        if error.errno != errno.ESRCH:
            raise

    # Dereference & force GC for a cleanup
    del proc
    del stdout
    del stderr
    gc.collect()

    runtime.stderr = "\n".join(result["stderr"])
    runtime.stdout = "\n".join(result["stdout"])
    runtime.merged = "\n".join(result["merged"])
    return runtime
