# (c) 2016 Anaconda, Inc. / https://anaconda.com
# All Rights Reserved
#
# constructor is distributed under the terms of the BSD 3-clause license.
# Consult LICENSE.txt or http://opensource.org/licenses/BSD-3-Clause.

import hashlib
import logging
import math
import re
import sys
from os import sep, unlink
from os.path import basename, isdir, isfile, islink, normpath
from shutil import rmtree

try:
    import yaml
except ImportError:
    import ruamel_yaml as yaml

logger = logging.getLogger(__name__)


def filename_dist(dist):
    """ Return the filename of a distribution. """
    if hasattr(dist, 'to_filename'):
        return dist.to_filename()
    else:
        return dist


def fill_template(data, d, exceptions=[]):
    pat = re.compile(r'__(\w+)__')

    def replace(match):
        key = match.group(1)
        return key if key in exceptions else d[key]

    return pat.sub(replace, data)


def hash_files(paths, algorithm='md5'):
    h = hashlib.new(algorithm)
    for path in paths:
        with open(path, 'rb') as fi:
            while True:
                chunk = fi.read(262144)
                if not chunk:
                    break
                h.update(chunk)
    return h.hexdigest()


def make_VIProductVersion(version):
    """
    always create a version of the form X.X.X.X
    """
    pat = re.compile(r'\d+$')
    res = []
    for part in version.split('.'):
        if pat.match(part):
            res.append(part)
    while len(res) < 4:
        res.append('0')
    return '.'.join(res[:4])


def read_ascii_only(path):
    with open(path) as fi:
        data = fi.read()
    for c in data:
        if ord(c) > 127:
            sys.exit("Error: unexpected non-ASCII character '%s' in: %s" %
                     (c, path))
    return data


if_pat = re.compile(r'^#if ([ \S]+)$\n'
                    r'(.*?)'
                    r'(^#else\s*$\n(.*?))?'
                    r'^#endif\s*$\n', re.M | re.S)


def preprocess(data, namespace):

    def if_repl(match):
        cond = match.group(1).strip()
        if eval(cond, namespace, {}):
            return match.group(2)
        else:
            return match.group(4) or ''

    return if_pat.sub(if_repl, data)


def add_condarc(info):
    condarc = info.get('condarc')
    if condarc is None:
        # The legacy approach
        write_condarc = info.get('write_condarc')
        default_channels = info.get('conda_default_channels')
        channel_alias = info.get('conda_channel_alias')
        channels = info.get('channels')
        if not (write_condarc and (default_channels or channels or channel_alias)):
            return
        condarc = {}
        if default_channels:
            condarc['default_channels'] = default_channels
        if channels:
            condarc['channels'] = channels
        if channel_alias:
            condarc['channel_alias'] = channel_alias
    if isinstance(condarc, dict):
        condarc = yaml.dump(condarc)
    yield '# ----- add condarc'
    if info['_platform'].startswith('win'):
        yield 'Var /Global CONDARC'
        yield 'FileOpen $CONDARC "$INSTDIR\\.condarc" w'
        for line in condarc.splitlines():
            yield 'FileWrite $CONDARC "%s$\\r$\\n"' % line
        yield 'FileClose $CONDARC'
    else:
        yield 'cat <<EOF >"$PREFIX/.condarc"'
        for line in condarc.splitlines():
            yield line
        yield 'EOF'


def ensure_transmuted_ext(info, url):
    """
    If transmuting, micromamba won't find the dist in the preconda tarball
    unless it has the (correct and transmuted) extension. Otherwise, the command
    `micromamba constructor --extract-tarballs` fails.
    Unfortunately this means the `urls` file might end up containing
    fake URLs, since those .conda archives might not really exist online,
    and they were only created locally.
    """
    if (
        info.get("transmute_file_type") == ".conda"
        and "micromamba" in basename(info.get("_conda_exe", ""))
    ):
        if url.lower().endswith(".tar.bz2"):
            url = url[:-8] + ".conda"
    return url


def get_final_url(info, url):
    mapping = info.get('channels_remap', [])
    for entry in mapping:
        src = entry['src']
        dst = entry['dest']
        if url.startswith(src):
            new_url = url.replace(src, dst)
            if url.endswith(".tar.bz2"):
                logger.warning("You need to make the package %s available "
                               "at %s", url.rsplit('/', 1)[1], new_url)
            return new_url
    return url


def get_final_channels(info):
    mapped_channels = []
    for channel in info.get('channels', []):
        url = get_final_url(info, channel)
        if url.startswith("file://"):
            logger.warning("local channel %s does not have a remap. "
                           "It will not be included in the installer", url)
            continue
        mapped_channels.append(url)
    return mapped_channels


def normalize_path(path):
    new_path = normpath(path)
    return new_path.replace(sep + sep, sep)


def rm_rf(path):
    """
    try to delete path, but never fail
    """
    try:
        if islink(path) or isfile(path):
            # Note that we have to check if the destination is a link because
            # exists('/path/to/dead-link') will return False, although
            # islink('/path/to/dead-link') is True.
            unlink(path)
        elif isdir(path):
            rmtree(path)
    except OSError:
        pass


def yield_lines(path):
    for line in open(path):
        line = line.strip()
        if not line or line.startswith('#'):
            continue
        yield line


def approx_size_kb(info, which="pkgs"):
    valid = ("pkgs", "tarballs", "total")
    assert which in valid, f"'which' must be one of {valid}"
    size_pkgs = info.get('_approx_pkgs_size', 0)
    size_tarballs = info.get('_approx_tarballs_size', 0)
    if which == "pkgs":
        size_bytes = size_pkgs
    elif which == "tarballs":
        size_bytes = size_tarballs
    else:
        size_bytes = size_pkgs + size_tarballs

    # division by 10^3 instead of 2^10 is deliberate here. gives us more room
    return int(math.ceil(size_bytes/1000))
