import contextlib
import errno
import fnmatch
import functools
import os
import shutil
import sys
import tempfile as _tempfile

from eth_utils import (
    to_tuple,
    is_string,
    is_list_like,
)


if sys.version_info.major == 2:
    FileNotFoundError = OSError


def ensure_path_exists(dir_path):
    """
    Make sure that a path exists
    """
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)
        return True
    return False


def ensure_file_exists(file_path):
    """
    Make sure that a path exists
    """
    if os.path.exists(file_path):
        return False
    base_dir = os.path.dirname(file_path)
    ensure_path_exists(base_dir)
    with open(file_path, 'w'):
        pass
    return True


def remove_file_if_exists(path):
    if os.path.isfile(path):
        os.remove(path)
        return True
    return False


def remove_dir_if_exists(path):
    if os.path.isdir(path):
        shutil.rmtree(path)
        return True
    return False


def mkdir(path):
    try:
        os.makedirs(path)
    except OSError as exc:  # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise


def is_executable_available(program):
    def is_exe(fpath):
        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

    fpath = os.path.dirname(program)
    if fpath:
        if is_exe(program):
            return True
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            path = path.strip('"')
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return True

    return False


@to_tuple
def recursive_find_files(base_dir, pattern):
    """
    Recursively traverse directory tree and find files matching pattern.

    :param base_dir: Base directory to start crawling.
    :param pattern: File pattern, to match again.
                    Either string (single pattern match) or tuple (multiple patterns)
    """

    def match(filename, pattern):
        if is_list_like(pattern):
            return any([fnmatch.fnmatch(filename, p) for p in pattern])
        elif is_string(pattern):
            return fnmatch.fnmatch(filename, pattern)
        else:
            raise TypeError(
                "Pattern must either be a string pattern or a list of patterns." +
                "  Got {0}".format(pattern)
            )

    for dirpath, _, filenames in os.walk(base_dir):
        for filename in filenames:
            if match(filename, pattern):
                yield os.path.join(dirpath, filename)


@contextlib.contextmanager
def tempdir(*args, **kwargs):
    directory = _tempfile.mkdtemp(*args, **kwargs)

    try:
        yield directory
    finally:
        remove_dir_if_exists(directory)


@contextlib.contextmanager
def tempfile(*args, **kwargs):
    _, file_path = _tempfile.mkstemp(*args, **kwargs)

    try:
        yield file_path
    finally:
        remove_file_if_exists(file_path)


def is_same_path(p1, p2):
    n_p1 = os.path.abspath(os.path.expanduser(p1))
    n_p2 = os.path.abspath(os.path.expanduser(p2))

    try:
        return os.path.samefile(n_p1, n_p2)
    except FileNotFoundError:
        return n_p1 == n_p2


def relpath(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        value = fn(*args, **kwargs)
        if is_string(value):
            return os.path.relpath(value)
        elif is_list_like(value):
            return type(value)([
                os.path.relpath(path) for path in value
            ])
        else:
            raise TypeError("Unsupported type: {0}".format(type(value)))
    return wrapper


def normpath(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        path = fn(*args, **kwargs)
        return os.path.normpath(path)
    return wrapper


def is_under_path(base_path, path):
    if is_same_path(base_path, path):
        return False
    absolute_base_path = os.path.abspath(base_path)
    absolute_path = os.path.abspath(path)
    return absolute_path.startswith(absolute_base_path)


def get_latest_mtime(file_paths):
    if not file_paths:
        return None
    else:
        return max(
            os.path.getmtime(path)
            for path
            in file_paths
        )
