import os
from collections import OrderedDict

from conans.client.graph.compute_pid import run_validate_package_id
from conans.client.loader import load_python_file
from conans.errors import conanfile_exception_formatter
from conans.util.files import save

# TODO: Define other compatibility besides applications
_default_compat = """\
# This file was generated by Conan. Remove this comment if you edit this file or Conan
# will destroy your changes.
from cppstd_compat import cppstd_compat


def compatibility(conanfile):
    configs = cppstd_compat(conanfile)
    # TODO: Append more configurations for your custom compatibility rules
    return configs
"""


_default_cppstd_compat = """\
# This file was generated by Conan. Remove this comment if you edit this file or Conan
# will destroy your changes.
from conan.tools.build import supported_cppstd


def cppstd_compat(conanfile):
    # It will try to find packages with all the cppstd versions

    compiler = conanfile.settings.get_safe("compiler")
    compiler_version = conanfile.settings.get_safe("compiler.version")
    cppstd = conanfile.settings.get_safe("compiler.cppstd")
    if not compiler or not compiler_version or not cppstd:
        return []
    base = dict(conanfile.settings.values_list)
    cppstd_possible_values = supported_cppstd(conanfile)
    ret = []
    for _cppstd in cppstd_possible_values:
        if _cppstd is None or _cppstd == cppstd:
            continue
        configuration = base.copy()
        configuration["compiler.cppstd"] = _cppstd
        ret.append({"settings": [(k, v) for k, v in configuration.items()]})

    return ret
"""


def get_binary_compatibility_file_paths(cache):
    compatible_folder = os.path.join(cache.plugins_path, "compatibility")
    compatibility_file = os.path.join(compatible_folder, "compatibility.py")
    cppstd_compat_file = os.path.join(compatible_folder, "cppstd_compat.py")
    return compatibility_file, cppstd_compat_file


def migrate_compatibility_files(cache):
    from conans.client.migrations import update_file

    compatibility_file, cppstd_compat_file = get_binary_compatibility_file_paths(cache)
    update_file(compatibility_file, _default_compat)
    update_file(cppstd_compat_file, _default_cppstd_compat)


class BinaryCompatibility:

    def __init__(self, cache):
        compatibility_file, cppstd_compat_file = get_binary_compatibility_file_paths(cache)
        mod, _ = load_python_file(compatibility_file)
        self._compatibility = mod.compatibility

    def compatibles(self, conanfile):
        compat_infos = []
        if hasattr(conanfile, "compatibility"):
            with conanfile_exception_formatter(conanfile, "compatibility"):
                recipe_compatibles = conanfile.compatibility()
                compat_infos.extend(self._compatible_infos(conanfile, recipe_compatibles))

        plugin_compatibles = self._compatibility(conanfile)
        compat_infos.extend(self._compatible_infos(conanfile, plugin_compatibles))
        if not compat_infos:
            return {}

        result = OrderedDict()
        original_info = conanfile.info
        original_settings = conanfile.settings
        original_options = conanfile.options
        for c in compat_infos:
            # we replace the conanfile, so ``validate()`` and ``package_id()`` can
            # use the compatible ones
            conanfile.info = c
            conanfile.settings = c.settings
            conanfile.options = c.options
            run_validate_package_id(conanfile)
            pid = c.package_id()
            if pid not in result and not c.invalid:
                result[pid] = c
        # Restore the original state
        conanfile.info = original_info
        conanfile.settings = original_settings
        conanfile.options = original_options
        return result

    @staticmethod
    def _compatible_infos(conanfile, compatibles):
        result = []
        if compatibles:
            for elem in compatibles:
                compat_info = conanfile.original_info.clone()
                settings = elem.get("settings")
                if settings:
                    compat_info.settings.update_values(settings)
                options = elem.get("options")
                if options:
                    compat_info.options.update(options_values=OrderedDict(options))
                result.append(compat_info)
        return result
