'''
Module to store conda build settings.
'''
from __future__ import absolute_import, division, print_function

import copy
from collections import namedtuple
import math
import os
from os.path import abspath, expanduser, join, expandvars
import shutil
import sys
import time

from .conda_interface import root_dir, root_writable
from .conda_interface import binstar_upload
from .variants import get_default_variant
from .conda_interface import cc_platform, cc_conda_build, subdir

from .utils import get_build_folders, rm_rf, get_logger, get_conda_operation_locks

on_win = (sys.platform == 'win32')

# Don't "save" an attribute of this module for later, like build_prefix =
# conda_build.config.config.build_prefix, as that won't reflect any mutated
# changes.

conda_build = "conda-build"


def _ensure_dir(path):
    # this can fail in parallel operation, depending on timing.  Just try to make the dir,
    #    but don't bail if fail.
    if not os.path.isdir(path):
        try:
            os.makedirs(path)
        except OSError:
            pass


# we need this to be accessible to the CLI, so it needs to be more static.
DEFAULT_PREFIX_LENGTH = 255

# translate our internal more meaningful subdirs to the ones that conda understands
SUBDIR_ALIASES = {
    'linux-cos5-x86_64': 'linux-64',
    'linux-cos6-x86_64': 'linux-64',
    'linux-cos5-x86': 'linux-32',
    'linux-cos6-x86': 'linux-32',
    'osx-109-x86_64': 'osx-64',
    'win-x86_64': 'win-64',
    'win-x86': 'win-32',
}


Setting = namedtuple("ConfigSetting", "name, default")
DEFAULTS = [Setting('activate', True),
            Setting('anaconda_upload', binstar_upload),
            Setting('force_upload', True),
            Setting('channel_urls', []),
            Setting('dirty', False),
            Setting('include_recipe', True),
            Setting('no_download_source', False),
            Setting('override_channels', False),
            Setting('skip_existing', False),
            Setting('token', None),
            Setting('user', None),
            Setting('verbose', True),

            Setting('debug', False),
            Setting('timeout', 90),
            Setting('set_build_id', True),
            Setting('disable_pip', False),
            Setting('_output_folder', None),
            Setting('prefix_length_fallback', True),
            Setting('_prefix_length', DEFAULT_PREFIX_LENGTH),
            Setting('long_test_prefix', True),
            Setting('locking', True),
            Setting('max_env_retry', 3),
            Setting('remove_work_dir', True),
            Setting('_host_platform', None),
            Setting('_host_arch', None),
            Setting('filename_hashing', cc_conda_build.get('filename_hashing',
                                                           'true').lower() == 'true'),
            Setting('keep_old_work', False),
            Setting('_src_cache_root', abspath(expanduser(expandvars(
                cc_conda_build.get('cache_dir')))) if cc_conda_build.get('cache_dir') else None),
            Setting('copy_test_source_files', True),

            # should rendering cut out any skipped metadata?
            Setting('trim_skip', True),

            # Disable the overlinking test for this package. This test checks that transitive DSOs
            # are not referenced by DSOs in the package being built. When this happens something
            # has gone wrong with:
            # 1. Linker flags not being passed, or not working correctly:
            #    (GNU ld: -as-needed, Apple ld64: -dead_strip_dylibs -no_implicit_dylibs)
            # 2. A missing package in reqs/run (maybe that package is missing run_exports?)
            # 3. A missing (or broken) CDT package in reqs/build or (on systems without CDTs)
            # 4. .. a missing value in the hard-coded but metadata-augmentable library whitelist
            # It is important that packages do not suffer from 2 because uninstalling that missing
            # package leads to an inability to run this package.
            #
            # default to not erroring with overlinking for now.  We have specified in
            #    cli/main_build.py that this default will switch in conda-build 4.0.
            Setting('error_overlinking', cc_conda_build.get('error_overlinking',
                                                           'false').lower() == 'true'),

            Setting('index', None),
            # support legacy recipes where only build is specified and expected to be the
            #    folder that packaging is done on
            Setting('build_is_host', False),

            # these are primarily for testing.  They override the native build platform/arch,
            #     which is useful in tests, but makes little sense on actual systems.
            Setting('_platform', None),
            Setting('_arch', None),
            Setting('_target_subdir', None),

            # variants
            Setting('variant_config_files', []),
            # this file precludes usage of any system-wide or cwd config files.  Config files in
            #    recipes are still respected, and they override this file.
            Setting('exclusive_config_file', None),
            Setting('ignore_system_variants', False),
            Setting('hash_length', 7),

            # append/clobber metadata section data (for global usage.  Can also add files to
            #    recipe.)
            Setting('append_sections_file', None),
            Setting('clobber_sections_file', None),
            Setting('bootstrap', None),

            # source provisioning.
            Setting('git_commits_since_tag', 0),

            # pypi upload settings (twine)
            Setting('password', None),
            Setting('sign', False),
            Setting('sign_with', 'gpg'),
            Setting('identity', None),
            Setting('config_file', None),
            Setting('repository', 'pypitest'),

            # conda-verify is disabled by default.  This is because it was inoperable for a long
            #     time, and thus needs to be opt-in not opt-out.
            Setting('verify', False),
            Setting('ignore_recipe_verify_scripts',
                  cc_conda_build.get('ignore_recipe_verify_scripts', [])),
            Setting('ignore_package_verify_scripts',
                    cc_conda_build.get('ignore_package_verify_scripts', [])),
            Setting('run_recipe_verify_scripts',
                    cc_conda_build.get('run_package_verify_scripts', [])),
            Setting('run_package_verify_scripts',
                    cc_conda_build.get('run_package_verify_scripts', [])),

            # Recipes that have no host section, only build, should bypass the build/host line.
            # This is to make older recipes still work with cross-compiling.  True cross-compiling
            # involving compilers (not just python) will still require recipe modification to have
            # distinct host and build sections, but simple python stuff should work without.
            Setting('merge_build_host', False),

            # path to output build statistics to
            Setting('stats_file', None),

            # extra deps to add to test env creation
            Setting('extra_deps', [])
            ]


def print_function_deprecation_warning(func):
    def func_wrapper(*args, **kw):
        log = get_logger(__name__)
        log.warn("WARNING: attribute {} is deprecated and will be removed in conda-build 4.0.  "
                "Please update your code - file issues on the conda-build issue tracker "
                 "if you need help.".format(func.__name__))
        return func(*args, **kw)
    return func_wrapper


class Config(object):
    __file__ = __path__ = __file__
    __package__ = __package__
    __doc__ = __doc__

    def __init__(self, variant=None, **kwargs):
        super(Config, self).__init__()
        # default variant is set in render's distribute_variants
        self.variant = variant or {}
        self.set_keys(**kwargs)
        if self._src_cache_root:
            self._src_cache_root = os.path.expanduser(self._src_cache_root)

    def _set_attribute_from_kwargs(self, kwargs, attr, default):
        value = kwargs.get(attr, getattr(self, attr) if hasattr(self, attr) else default)
        setattr(self, attr, value)
        if attr in kwargs:
            del kwargs[attr]

    def set_keys(self, **kwargs):
        def env(lang, default):
            version = kwargs.pop(lang, None)
            if not version:
                # Hooray for corner cases.
                if lang == 'python':
                    lang = 'py'
                elif lang == 'numpy':
                    lang = 'npy'
                elif lang == 'r_base':
                    lang = 'r'
                var = 'CONDA_' + lang.upper()
                version = os.getenv(var) if os.getenv(var) else default
            elif isinstance(version, list) and len(version) == 1:
                version = version[0]
            return version

        def set_lang(variant, lang):
            value = env(lang, self.variant.get(lang))
            if value:
                if '.' not in str(value):
                    value = '.'.join((value[0], value[1:]))
                variant[lang] = value

        # this is where we override any variant config files with the legacy CONDA_* vars
        #     or CLI params
        for lang in ('perl', 'lua', 'python', 'numpy', 'r_base'):
            set_lang(self.variant, lang)

        self._build_id = kwargs.pop('build_id', getattr(self, '_build_id', ""))
        source_cache = kwargs.pop('cache_dir', None)
        croot = kwargs.pop('croot', None)

        if source_cache:
            self._src_cache_root = os.path.abspath(os.path.normpath(
                os.path.expanduser(source_cache)))
        if croot:
            self._croot = os.path.abspath(os.path.normpath(croot))
        else:
            # set default value (not actually None)
            self._croot = getattr(self, '_croot', None)

        # handle known values better than unknown (allow defaults)
        for value in DEFAULTS:
            self._set_attribute_from_kwargs(kwargs, value.name, value.default)

        # dangle remaining keyword arguments as attributes on this class
        for name, value in kwargs.items():
            setattr(self, name, value)

    @property
    def arch(self):
        """Always the native (build system) arch, except when pretending to be some
        other platform"""
        return self._arch or subdir.rsplit('-', 1)[1]

    @arch.setter
    def arch(self, value):
        log = get_logger(__name__)
        log.warn("setting build arch.  This is only useful when pretending to be on another "
                 "arch, such as for rendering necessary dependencies on a non-native arch."
                 "  I trust that you know what you're doing.")
        self._arch = str(value)

    @property
    def platform(self):
        """Always the native (build system) OS, except when pretending to be some
        other platform"""
        return self._platform or subdir.rsplit('-', 1)[0]

    @platform.setter
    def platform(self, value):
        log = get_logger(__name__)
        log.warn("setting build platform. This is only useful when "
                "pretending to be on another " "another platform, such as "
                "for rendering necessary dependencies on a non-native "
                "platform. I trust that you know what you're doing.")
        if value == 'noarch':
            raise ValueError("config platform should never be noarch.  Set host_platform instead.")
        self._platform = value

    @property
    def build_subdir(self):
        """Determines channel to download build env packages from.
        Should generally be the native platform.  Does not preclude packages from noarch."""
        return '-'.join((self.platform, self.arch))

    @property
    def host_arch(self):
        return (self._host_arch or
                self.variant.get('target_platform', self.build_subdir).split('-', 1)[1])

    @host_arch.setter
    def host_arch(self, value):
        self._host_arch = value

    @property
    def noarch(self):
        return self.host_platform == 'noarch'

    def reset_platform(self):
        if not self.platform == cc_platform:
            self.platform = cc_platform

    @property
    def subdir(self):
        return "-".join([self.platform, str(self.arch)])

    @property
    def host_platform(self):
        return (self._host_platform or
                self.variant.get('target_platform', self.build_subdir).split('-', 1)[0])

    @host_platform.setter
    def host_platform(self, value):
        self._host_platform = value

    @property
    def host_subdir(self):
        subdir = self.variant.get('target_platform', self.build_subdir)
        if self.host_platform == 'noarch':
            subdir = self.host_platform
        elif subdir != "-".join([self.host_platform, str(self.host_arch)]):
            subdir = "-".join([self.host_platform, str(self.host_arch)])
        return SUBDIR_ALIASES.get(subdir, subdir)

    @host_subdir.setter
    def host_subdir(self, value):
        value = SUBDIR_ALIASES.get(value, value)
        values = value.rsplit('-', 1)
        self.host_platform = values[0]
        if len(values) > 1:
            self.host_arch = values[1]

    @property
    def target_subdir(self):
        return self._target_subdir or self.host_subdir

    @target_subdir.setter
    def target_subdir(self, value):
        self._target_subdir = value

    @property
    def src_cache_root(self):
        return self._src_cache_root if self._src_cache_root else self.croot

    @src_cache_root.setter
    def src_cache_root(self, value):
        self._src_cache_root = value

    @property
    def croot(self):
        """This is where source caches and work folders live"""
        if not self._croot:
            _bld_root_env = os.getenv('CONDA_BLD_PATH')
            _bld_root_rc = cc_conda_build.get('root-dir')
            if _bld_root_env:
                self._croot = abspath(expanduser(_bld_root_env))
            elif _bld_root_rc:
                self._croot = abspath(expanduser(expandvars(_bld_root_rc)))
            elif root_writable:
                self._croot = join(root_dir, 'conda-bld')
            else:
                self._croot = abspath(expanduser('~/conda-bld'))
        return self._croot

    @croot.setter
    def croot(self, croot):
        """Set croot - if None is passed, then the default value will be used"""
        self._croot = croot

    @property
    def output_folder(self):
        return self._output_folder or self.croot

    @output_folder.setter
    def output_folder(self, value):
        self._output_folder = value

    @property
    def build_folder(self):
        """This is the core folder for a given build.
        It has the environments and work directories."""
        return os.path.join(self.croot, self.build_id)

    # back compat for conda-build-all - expects CONDA_* vars to be attributes of the config object
    @property
    @print_function_deprecation_warning
    def CONDA_LUA(self):
        return self.variant.get('lua', get_default_variant(self)['lua'])

    @CONDA_LUA.setter
    @print_function_deprecation_warning
    def CONDA_LUA(self, value):
        self.variant['lua'] = value

    @property
    @print_function_deprecation_warning
    def CONDA_PY(self):
        value = self.variant.get('python', get_default_variant(self)['python'])
        return int(''.join(value.split('.')))

    @CONDA_PY.setter
    @print_function_deprecation_warning
    def CONDA_PY(self, value):
        value = str(value)
        self.variant['python'] = '.'.join((value[0], value[1:]))

    @property
    @print_function_deprecation_warning
    def CONDA_NPY(self):
        value = self.variant.get('numpy', get_default_variant(self)['numpy'])
        return int(''.join(value.split('.')))

    @CONDA_NPY.setter
    @print_function_deprecation_warning
    def CONDA_NPY(self, value):
        value = str(value)
        self.variant['numpy'] = '.'.join((value[0], value[1:]))

    @property
    @print_function_deprecation_warning
    def CONDA_PERL(self):
        return self.variant.get('perl', get_default_variant(self)['perl'])

    @CONDA_PERL.setter
    @print_function_deprecation_warning
    def CONDA_PERL(self, value):
        self.variant['perl'] = value

    @property
    @print_function_deprecation_warning
    def CONDA_R(self):

        return self.variant.get('r_base', get_default_variant(self)['r_base'])

    @CONDA_R.setter
    @print_function_deprecation_warning
    def CONDA_R(self, value):
        self.variant['r_base'] = value

    def _get_python(self, prefix, platform):
        if platform.startswith('win'):
            if os.path.isfile(os.path.join(prefix, 'python_d.exe')):
                res = join(prefix, 'python_d.exe')
            else:
                res = join(prefix, 'python.exe')
        else:
            res = join(prefix, 'bin/python')
        return res

    def _get_perl(self, prefix, platform):
        if platform.startswith('win'):
            res = join(prefix, 'Library', 'bin', 'perl.exe')
        else:
            res = join(prefix, 'bin/perl')
        return res

    # TODO: This is probably broken on Windows, but no one has a lua package on windows to test.
    def _get_lua(self, prefix, platform):
        lua_ver = self.variant.get('lua', get_default_variant(self)['lua'])
        binary_name = "luajit" if (lua_ver and lua_ver[0] == "2") else "lua"
        if platform.startswith('win'):
            res = join(prefix, 'Library', 'bin', '{}.exe'.format(binary_name))
        else:
            res = join(prefix, 'bin/{}'.format(binary_name))
        return res

    def _get_r(self, prefix, platform):
        if platform.startswith('win'):
            res = join(prefix, 'Scripts', 'R.exe')
            # MRO test:
            if not os.path.exists(res):
                res = join(prefix, 'bin', 'R.exe')
        else:
            res = join(prefix, 'bin', 'R')
        return res

    def compute_build_id(self, package_name, reset=False):
        if self.set_build_id and (not self._build_id or reset):
            assert not os.path.isabs(package_name), ("package name should not be a absolute path, "
                                                     "to preserve croot during path joins")
            folder_basenames = [os.path.basename(fldr) for fldr in get_build_folders(self.croot)]
            build_folders = sorted([build_folder for build_folder in folder_basenames
                            if build_folder[:build_folder.rfind('_')] == package_name])
            if self.dirty and build_folders:
                # Use the most recent build with matching recipe name
                self._build_id = build_folders[-1]
            else:
                old_dir = self.work_dir if len(os.listdir(self.work_dir)) > 0 else ""
                # here we uniquely name folders, so that more than one build can happen concurrently
                #    keep 6 decimal places so that prefix < 80 chars
                build_id = package_name + "_" + str(int(time.time() * 1000))
                # important: this is recomputing prefixes and determines where work folders are.
                self._build_id = build_id
                if old_dir:
                    shutil.move(old_dir, self.work_dir)

    @property
    def build_id(self):
        """This is a per-build (almost) unique id, consisting of the package being built, and the
        time since the epoch, in ms.  It is appended to build and test prefixes, and used to create
        unique work folders for build and test."""
        return self._build_id

    @build_id.setter
    def build_id(self, _build_id):
        _build_id = _build_id.rstrip("/").rstrip("\\")
        assert not os.path.isabs(_build_id), ("build_id should not be an absolute path, "
                                              "to preserve croot during path joins")
        self._build_id = _build_id

    @property
    def prefix_length(self):
        return self._prefix_length

    @prefix_length.setter
    def prefix_length(self, length):
        self._prefix_length = length

    @property
    def _short_host_prefix(self):
        return join(self.build_folder, '_h_env')

    @property
    def _long_host_prefix(self):
        placeholder_length = self.prefix_length - len(self._short_host_prefix)
        placeholder = '_placehold'
        repeats = int(math.ceil(placeholder_length / len(placeholder)) + 1)
        placeholder = (self._short_host_prefix + repeats * placeholder)[:self.prefix_length]
        return max(self._short_host_prefix, placeholder)

    @property
    def build_prefix(self):
        """The temporary folder where the build environment is created.  The build env contains
        libraries that may be linked, but only if the host env is not specified.  It is placed on
        PATH."""
        if self.build_is_host:
            prefix = self.host_prefix
        else:
            prefix = join(self.build_folder, '_build_env')
        return prefix

    @property
    def host_prefix(self):
        """The temporary folder where the host environment is created.  The host env contains
        libraries that may be linked.  It is not placed on PATH."""
        if on_win:
            return self._short_host_prefix
        return self._long_host_prefix

    @property
    def _short_test_prefix(self):
        return join(self.build_folder, '_test_env')

    def _long_prefix(self, base_prefix):
        placeholder_length = self.prefix_length - len(base_prefix)
        placeholder = '_placehold'
        repeats = int(math.ceil(placeholder_length / len(placeholder)) + 1)
        placeholder = (base_prefix + repeats * placeholder)[:self.prefix_length]
        return max(base_prefix, placeholder)

    @property
    def test_prefix(self):
        """The temporary folder where the test environment is created"""
        if on_win or not self.long_test_prefix:
            return self._short_test_prefix
        return self._long_prefix(self._short_test_prefix)

    @property
    def build_python(self):
        return self.python_bin(self.build_prefix, self.platform)

    @property
    def host_python(self):
        return self._get_python(self.host_prefix, self.host_platform)

    @property
    def test_python(self):
        return self.python_bin(self.test_prefix, self.host_platform)

    def python_bin(self, prefix, platform):
        return self._get_python(prefix, platform)

    def perl_bin(self, prefix, platform):
        return self._get_perl(prefix, platform)

    def lua_bin(self, prefix, platform):
        return self._get_lua(prefix, platform)

    def r_bin(self, prefix, platform):
        return self._get_r(prefix, platform)

    @property
    def info_dir(self):
        """Path to the info dir in the build prefix, where recipe metadata is stored"""
        path = join(self.host_prefix, 'info')
        _ensure_dir(path)
        return path

    @property
    def meta_dir(self):
        """Path to the conda-meta dir in the build prefix, where package index json files are
        stored"""
        path = join(self.host_prefix, 'conda-meta')
        _ensure_dir(path)
        return path

    @property
    def broken_dir(self):
        """Where packages that fail the test phase are placed"""
        path = join(self.croot, "broken")
        _ensure_dir(path)
        return path

    @property
    def bldpkgs_dir(self):
        """ Dir where the package is saved. """
        path = join(self.croot, self.host_subdir)
        _ensure_dir(path)
        return path

    @property
    def bldpkgs_dirs(self):
        """ Dirs where previous build packages might be. """
        # The first two *might* be the same, but might not, depending on if this is a cross-compile.
        #     subdir should be the native platform, while self.subdir would be the host platform.
        return {join(self.croot, self.host_subdir), join(self.croot, subdir),
                join(self.croot, "noarch"), }

    @property
    def src_cache(self):
        """Where tarballs and zip files are downloaded and stored"""
        path = join(self.src_cache_root, 'src_cache')
        _ensure_dir(path)
        return path

    @property
    def git_cache(self):
        """Where local clones of git sources are stored"""
        path = join(self.src_cache_root, 'git_cache')
        _ensure_dir(path)
        return path

    @property
    def hg_cache(self):
        """Where local clones of hg sources are stored"""
        path = join(self.src_cache_root, 'hg_cache')
        _ensure_dir(path)
        return path

    @property
    def svn_cache(self):
        """Where local checkouts of svn sources are stored"""
        path = join(self.src_cache_root, 'svn_cache')
        _ensure_dir(path)
        return path

    @property
    def work_dir(self):
        """Where the source for the build is extracted/copied to."""
        path = join(self.build_folder, 'work')
        _ensure_dir(path)
        # if os.path.isdir(path):
        #     lst = [fn for fn in os.listdir(path) if not fn.startswith('.')]
        #     if len(lst) == 1:
        #         dir_path = join(path, lst[0])
        #         if os.path.isdir(dir_path):
        #             return dir_path
        return path

    @property
    def test_dir(self):
        """The temporary folder where test files are copied to, and where tests start execution"""
        path = join(self.build_folder, 'test_tmp')
        _ensure_dir(path)
        return path

    def clean(self, remove_folders=True):
        # build folder is the whole burrito containing envs and source folders
        #   It will only exist if we download source, or create a build or test environment
        if remove_folders and not getattr(self, 'dirty'):
            if self.build_id:
                if os.path.isdir(self.build_folder):
                    rm_rf(self.build_folder)
            else:
                for path in [self.work_dir, self.test_dir, self.build_prefix, self.test_prefix]:
                    if os.path.isdir(path):
                        rm_rf(path)
            if os.path.isfile(os.path.join(self.build_folder, 'prefix_files')):
                rm_rf(os.path.join(self.build_folder, 'prefix_files'))
        else:
            print("\nLeaving build/test directories:"
                  "\n  Work:\t", self.work_dir,
                  "\n  Test:\t", self.test_dir,
                  "\nLeaving build/test environments:"
                  "\n  Test:\tsource activate ", self.test_prefix,
                  "\n  Build:\tsource activate ", self.build_prefix,
                  "\n\n")

        for lock in get_conda_operation_locks(self.locking, self.bldpkgs_dirs):
            if os.path.isfile(lock.lock_file):
                rm_rf(lock.lock_file)

    def clean_pkgs(self):
        for folder in self.bldpkgs_dirs:
            rm_rf(folder)

    def copy(self):
        new = copy.copy(self)
        new.variant = copy.deepcopy(self.variant)
        if hasattr(self, 'variants'):
            new.variants = copy.deepcopy(self.variants)
        return new

    # context management - automatic cleanup if self.dirty or self.keep_old_work is not True
    def __enter__(self):
        pass

    def __exit__(self, e_type, e_value, traceback):
        if not getattr(self, 'dirty') and e_type is None and not getattr(self, 'keep_old_work'):
            get_logger(__name__).info("--dirty flag and --keep-old-work not specified."
                                        "Removing build/test folder after successful build/test.\n")
            self.clean()
        else:
            self.clean(remove_folders=False)


def get_or_merge_config(config, variant=None, **kwargs):
    """Always returns a new object - never changes the config that might be passed in."""
    if not config:
        config = Config(variant=variant)
    else:
        # decouple this config from whatever was fed in.  People must change config by
        #    accessing and changing this attribute.
        config = config.copy()
    if kwargs:
        config.set_keys(**kwargs)
    if variant:
        config.variant.update(variant)
    return config


# legacy exports for conda
croot = Config().croot
