# ----------------------------------------------------------------------------
# Copyright (c) 2016-2022, QIIME 2 development team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------

import os
import sys
import errno
import shutil

import threading
import contextlib

_REDIRECTED_STDIO_LOCK = threading.Lock()


@contextlib.contextmanager
def redirected_stdio(stdout=None, stderr=None):
    with _REDIRECTED_STDIO_LOCK:
        if stdout is not None:
            with _redirected_fd(to=stdout, stdio=sys.stdout):
                if stderr is not None:
                    with _redirected_fd(to=stderr, stdio=sys.stderr):
                        yield
                else:
                    yield
        elif stderr is not None:
            with _redirected_fd(to=stderr, stdio=sys.stderr):
                yield
        else:
            yield


# Taken whole-sale from: http://stackoverflow.com/a/22434262/579416
@contextlib.contextmanager
def _redirected_fd(to=os.devnull, stdio=None):
    if stdio is None:
        stdio = sys.stdout

    stdio_fd = _get_fileno(stdio)
    # copy stdio_fd before it is overwritten
    # NOTE: `copied` is inheritable on Windows when duplicating a standard
    # stream
    with os.fdopen(os.dup(stdio_fd), 'wb') as copied:
        stdio.flush()  # flush library buffers that dup2 knows nothing about
        try:
            os.dup2(_get_fileno(to), stdio_fd)  # $ exec >&to
        except ValueError:  # filename
            with open(to, 'wb') as to_file:
                os.dup2(to_file.fileno(), stdio_fd)  # $ exec > to
        try:
            yield stdio  # allow code to be run with the redirected stdio
        finally:
            # restore stdio to its previous value
            # NOTE: dup2 makes stdio_fd inheritable unconditionally
            stdio.flush()
            os.dup2(copied.fileno(), stdio_fd)  # $ exec >&copied


def _get_fileno(file_or_fd):
    fd = getattr(file_or_fd, 'fileno', lambda: file_or_fd)()
    if not isinstance(fd, int):
        raise ValueError("Expected a file (`.fileno()`) or a file descriptor")
    return fd


def duplicate(src, dst):
    """Alternative to shutil.copyfile, this will use os.link when possible.

    See shutil.copyfile for documention. Only `src` and `dst` are supported.
    Unlike copyfile, this will not overwrite the destination if it exists.

    """
    if os.path.isdir(src):
        # os.link will give a permission error
        raise OSError(errno.EISDIR, "Is a directory", src)
    if os.path.isdir(dst):
        # os.link will give a FileExists error
        raise OSError(errno.EISDIR, "Is a directory", dst)

    if os.path.exists(dst):
        # shutil.copyfile will overwrite the existing file
        raise OSError(errno.EEXIST, "File exists", src, "File exists", dst)

    try:
        os.link(src, dst)
    except OSError as e:
        if e.errno == errno.EXDEV:  # Invalid cross-device link
            shutil.copyfile(src, dst)
        elif e.errno == errno.EPERM:  # Permissions/ownership error
            shutil.copyfile(src, dst)
        else:
            raise
