# -*- coding: utf-8 -*-
# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import absolute_import, division, print_function, unicode_literals

from errno import ENOENT
import fnmatch
from logging import getLogger
from os import rename, unlink, walk, makedirs, getcwd, rmdir, listdir
from os.path import abspath, dirname, isdir, join, split, exists
import shutil
from subprocess import Popen, PIPE, check_call, CalledProcessError
import sys

from . import MAX_TRIES, exp_backoff_fn
from .link import islink, lexists
from .permissions import make_writable, recursive_make_writable
from ...base.context import context
from ...common.compat import on_win
from ...common.constants import CONDA_TEMP_EXTENSION
from ...exceptions import CondaDependencyError
if not on_win:
    from ...common.path import which


log = getLogger(__name__)


def rmtree(path, *args, **kwargs):
    # subprocessing to delete large folders can be quite a bit faster
    if on_win:
        try:
            check_call('rd /s /q "{}"'.format(path), shell=True)
        except CalledProcessError as e:
            if e.returncode == 5:
                # skip permission errors
                log.warn("")
                pass
            else:
                raise
    else:
        try:
            makedirs('.empty')
        except:
            pass
        # yes, this looks strange.  See
        #    https://unix.stackexchange.com/a/79656/34459
        #    https://web.archive.org/web/20130929001850/http://linuxnote.net/jianingy/en/linux/a-fast-way-to-remove-huge-number-of-files.html
        rsync = which('rsync')
        if rsync:
            try:
                check_call([rsync, '-a', '--delete', join(getcwd(), '.empty') + "/", path + "/"])
            except CalledProcessError:
                log.warn("Removing folder {} the fast way (with rsync) failed.  "
                         "Do you have rsync available?".format(path))
                shutil.rmtree(path)
        else:
            shutil.rmtree(path)
        shutil.rmtree('.empty')


def unlink_or_rename_to_trash(path):
    """If files are in use, especially on windows, we can't remove them.
    The fallback path is to rename them (but keep their folder the same),
    which maintains the file handle validity.  See comments at:
    https://serverfault.com/a/503769
    """
    try:
        make_writable(path)
        unlink(path)
    except EnvironmentError:
        if on_win:
            # on windows, it is important to use the rename program, as just using python's
            #    rename leads to permission errors when files are in use.
            condabin_dir = join(context.conda_prefix, "condabin")
            trash_script = join(condabin_dir, 'rename_tmp.bat')
            if not exists(trash_script):
                raise CondaDependencyError("{} is missing.  Conda was not installed correctly.  "
                                           "Please file an issue on the conda github repo.".format(
                                               trash_script))
            _dirname, _fn = split(path)
            p = Popen(['cmd.exe', '/C', trash_script, _dirname, _fn, _fn + ".trash"],
                      stdout=PIPE, stderr=PIPE)
            stdout, stderr = p.communicate()
        else:
            rename(path, path + ".trash")


def remove_empty_parent_paths(path):
    # recurse to clean up empty folders that were created to have a nested hierarchy
    parent_path = dirname(path)
    while(isdir(parent_path) and not listdir(parent_path)):
        rmdir(parent_path)
        parent_path = dirname(parent_path)


def rm_rf(path, max_retries=5, trash=True, clean_empty_parents=False, *args, **kw):
    """
    Completely delete path
    max_retries is the number of times to retry on failure. The default is 5. This only applies
    to deleting a directory.
    If removing path fails and trash is True, files will be moved to the trash directory.
    """
    try:
        path = abspath(path)
        log.trace("rm_rf %s", path)
        if isdir(path) and not islink(path):
            backoff_rmdir(path)
        elif lexists(path):
            unlink_or_rename_to_trash(path)
        else:
            log.trace("rm_rf failed. Not a link, file, or directory: %s", path)
    finally:
        if lexists(path):
            log.info("rm_rf failed for %s", path)
            return False
    if clean_empty_parents:
        remove_empty_parent_paths(path)
    return True


# aliases that all do the same thing (legacy compat)
try_rmdir_all_empty = move_to_trash = move_path_to_trash = rm_rf


def delete_trash(prefix):
    if not prefix:
        prefix = sys.prefix
    exclude = set(['envs'])
    for root, dirs, files in walk(prefix, topdown=True):
        dirs[:] = [d for d in dirs if d not in exclude]
        for basename in files:
            if (fnmatch.fnmatch(basename, "*.trash") or
                    fnmatch.fnmatch(basename, "*" + CONDA_TEMP_EXTENSION)):
                filename = join(root, basename)
                try:
                    unlink(filename)
                    remove_empty_parent_paths(filename)
                except (OSError, IOError) as e:
                    log.debug("%r errno %d\nCannot unlink %s.", e, e.errno, filename)


def backoff_rmdir(dirpath, max_tries=MAX_TRIES):
    if not isdir(dirpath):
        return

    def retry(func, path, exc_info):
        if getattr(exc_info[1], 'errno', None) == ENOENT:
            return
        recursive_make_writable(dirname(path), max_tries=max_tries)
        func(path)

    def _rmdir(path):
        try:
            recursive_make_writable(path)
            exp_backoff_fn(rmtree, path, onerror=retry, max_tries=max_tries)
        except (IOError, OSError) as e:
            if e.errno == ENOENT:
                log.trace("no such file or directory: %s", path)
            else:
                raise
    try:
        rmtree(dirpath)
    # we don't really care about errors that much.  We'll catch remaining files
    #    with slower python logic.
    except:
        pass

    for root, dirs, files in walk(dirpath, topdown=False):
        for file in files:
            unlink_or_rename_to_trash(join(root, file))
        for dir in dirs:
            _rmdir(join(root, dir))

    _rmdir(dirpath)
    if isdir(dirpath):
        rmdir(dirpath)


def path_is_clean(path):
    """Sometimes we can't completely remove a path because files are considered in use
    by python (hardlinking confusion).  For our tests, it is sufficient that either the
    folder doesn't exist, or nothing but temporary file copies are left."""
    clean = not exists(path)
    if not clean:
        for root, dirs, fns in walk(path):
            for fn in fns:
                if not (fnmatch.fnmatch(fn, "*.trash") or
                        fnmatch.fnmatch(fn, "*" + CONDA_TEMP_EXTENSION)):
                    return False
    return True
