import os
import re
from collections import OrderedDict
from fnmatch import translate

from conans.errors import ConanException, RecipeNotFoundException
from conans.model.info import ConanInfo
from conans.model.ref import ConanFileReference, PackageReference
from conans.paths import CONANINFO
from conans.search.query_parse import evaluate_postfix, infix_to_postfix
from conans.util.files import load
from conans.util.log import logger


def filter_outdated(packages_infos, recipe_hash):
    if not recipe_hash:
        return packages_infos
    result = OrderedDict()
    for package_id, info in packages_infos.items():
        try:  # Existing package_info of old package might not have recipe_hash
            if info["recipe_hash"] != recipe_hash:
                result[package_id] = info
        except KeyError:
            pass
    return result


def filter_by_revision(metadata, packages_infos):
    ok = OrderedDict()
    recipe_revision = metadata.recipe.revision
    for package_id, info in packages_infos.items():
        try:
            rec_rev = metadata.packages[package_id].recipe_revision
            if rec_rev == recipe_revision:
                ok[package_id] = info
        except KeyError:
            pass
    return ok


def filter_packages(query, package_infos):
    if query is None:
        return package_infos
    try:
        if "!" in query:
            raise ConanException("'!' character is not allowed")
        if " not " in query or query.startswith("not "):
            raise ConanException("'not' operator is not allowed")
        postfix = infix_to_postfix(query) if query else []
        result = OrderedDict()
        for package_id, info in package_infos.items():
            if _evaluate_postfix_with_info(postfix, info):
                result[package_id] = info
        return result
    except Exception as exc:
        raise ConanException("Invalid package query: %s. %s" % (query, exc))


def _evaluate_postfix_with_info(postfix, conan_vars_info):

    # Evaluate conaninfo with the expression

    def evaluate_info(expression):
        """Receives an expression like compiler.version="12"
        Uses conan_vars_info in the closure to evaluate it"""
        name, value = expression.split("=", 1)
        value = value.replace("\"", "")
        return _evaluate(name, value, conan_vars_info)

    return evaluate_postfix(postfix, evaluate_info)


def _evaluate(prop_name, prop_value, conan_vars_info):
    """
    Evaluates a single prop_name, prop_value like "os", "Windows" against
    conan_vars_info.serialize_min()
    """

    def compatible_prop(setting_value, _prop_value):
        return (_prop_value == setting_value) or (_prop_value == "None" and setting_value is None)

    info_settings = conan_vars_info.get("settings", [])
    info_options = conan_vars_info.get("options", [])
    properties = ["os", "os_build", "compiler", "arch", "arch_build", "build_type"]

    def starts_with_common_settings(_prop_name):
        return any(_prop_name.startswith(setting + '.') for setting in properties)

    if prop_name in properties or starts_with_common_settings(prop_name):
        return compatible_prop(info_settings.get(prop_name, None), prop_value)
    else:
        return compatible_prop(info_options.get(prop_name, None), prop_value)


def search_recipes(cache, pattern=None, ignorecase=True):
    # Conan references in main storage
    no_user_channel = False
    if pattern:
        if isinstance(pattern, ConanFileReference):
            pattern = repr(pattern)
        if pattern.endswith("@"):  # packages without user/channel:
            no_user_channel = True
            pattern = pattern[:-1]
        pattern = translate(pattern)
        pattern = re.compile(pattern, re.IGNORECASE) if ignorecase else re.compile(pattern)

    refs = cache.all_refs()
    if no_user_channel:
        refs = [r for r in refs if r.user is None and r.channel is None]
    refs.extend(cache.editable_packages.edited_refs.keys())
    if pattern:
        refs = [r for r in refs if _partial_match(pattern, repr(r))]
    refs = sorted(refs)
    return refs


def _partial_match(pattern, reference):
    """
    Finds if pattern matches any of partial sums of tokens of conan reference
    """
    tokens = reference.replace('/', ' / ').replace('@', ' @ ').replace('#', ' # ').split()

    def partial_sums(iterable):
        partial = ''
        for i in iterable:
            partial += i
            yield partial

    return any(map(pattern.match, list(partial_sums(tokens))))


def search_packages(package_layout, query):
    """ Return a dict like this:

            {package_ID: {name: "OpenCV",
                           version: "2.14",
                           settings: {os: Windows}}}
    param package_layout: Layout for the given reference
    """
    if not os.path.exists(package_layout.base_folder()) or (
            package_layout.ref.revision and
            package_layout.recipe_revision() != package_layout.ref.revision):
        raise RecipeNotFoundException(package_layout.ref, print_rev=True)
    infos = _get_local_infos_min(package_layout)
    return filter_packages(query, infos)


def _get_local_infos_min(package_layout):
    result = OrderedDict()

    package_ids = package_layout.package_ids()
    for package_id in package_ids:
        # Read conaninfo
        pref = PackageReference(package_layout.ref, package_id)
        info_path = os.path.join(package_layout.package(pref), CONANINFO)
        if not os.path.exists(info_path):
            logger.error("There is no ConanInfo: %s" % str(info_path))
            continue
        conan_info_content = load(info_path)

        info = ConanInfo.loads(conan_info_content)
        if package_layout.ref.revision:
            metadata = package_layout.load_metadata()
            recipe_revision = metadata.packages[package_id].recipe_revision
            if recipe_revision and recipe_revision != package_layout.ref.revision:
                continue
        conan_vars_info = info.serialize_min()
        result[package_id] = conan_vars_info

    return result
