# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2017 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
"""
Parser for the environment markers micro-language defined in PEP 508.
"""

# Note: In PEP 345, the micro-language was Python compatible, so the ast
# module could be used to parse it. However, PEP 508 introduced operators such
# as ~= and === which aren't in Python, necessitating a different approach.

import os
import re
import sys
import platform

from .compat import string_types
from .util import in_venv, parse_marker
from .version import NormalizedVersion as NV

__all__ = ["interpret"]

_VERSION_PATTERN = re.compile(
    r"((\d+(\.\d+)*\w*)|\'(\d+(\.\d+)*\w*)\'|\"(\d+(\.\d+)*\w*)\")"
)


def _is_literal(o):
    if not isinstance(o, string_types) or not o:
        return False
    return o[0] in "'\""


def _get_versions(s):
    result = []
    for m in _VERSION_PATTERN.finditer(s):
        result.append(NV(m.groups()[0]))
    return set(result)


class Evaluator(object):
    """
    This class is used to evaluate marker expessions.
    """

    operations = {
        "==": lambda x, y: x == y,
        "===": lambda x, y: x == y,
        "~=": lambda x, y: x == y or x > y,
        "!=": lambda x, y: x != y,
        "<": lambda x, y: x < y,
        "<=": lambda x, y: x == y or x < y,
        ">": lambda x, y: x > y,
        ">=": lambda x, y: x == y or x > y,
        "and": lambda x, y: x and y,
        "or": lambda x, y: x or y,
        "in": lambda x, y: x in y,
        "not in": lambda x, y: x not in y,
    }

    def evaluate(self, expr, context):
        """
        Evaluate a marker expression returned by the :func:`parse_requirement`
        function in the specified context.
        """
        if isinstance(expr, string_types):
            if expr[0] in "'\"":
                result = expr[1:-1]
            else:
                if expr not in context:
                    raise SyntaxError("unknown variable: %s" % expr)
                result = context[expr]
        else:
            assert isinstance(expr, dict)
            op = expr["op"]
            if op not in self.operations:
                raise NotImplementedError("op not implemented: %s" % op)
            elhs = expr["lhs"]
            erhs = expr["rhs"]
            if _is_literal(expr["lhs"]) and _is_literal(expr["rhs"]):
                raise SyntaxError("invalid comparison: %s %s %s" % (elhs, op, erhs))

            lhs = self.evaluate(elhs, context)
            rhs = self.evaluate(erhs, context)
            if (elhs == "python_version" or erhs == "python_version") and op in (
                "<",
                "<=",
                ">",
                ">=",
                "===",
                "==",
                "!=",
                "~=",
            ):
                lhs = NV(lhs)
                rhs = NV(rhs)
            elif elhs == "python_version" and op in ("in", "not in"):
                lhs = NV(lhs)
                rhs = _get_versions(rhs)
            result = self.operations[op](lhs, rhs)
        return result


_DIGITS = re.compile(r"\d+\.\d+")


def default_context():
    def format_full_version(info):
        version = "%s.%s.%s" % (info.major, info.minor, info.micro)
        kind = info.releaselevel
        if kind != "final":
            version += kind[0] + str(info.serial)
        return version

    if hasattr(sys, "implementation"):
        implementation_version = format_full_version(sys.implementation.version)
        implementation_name = sys.implementation.name
    else:
        implementation_version = "0"
        implementation_name = ""

    ppv = platform.python_version()
    m = _DIGITS.match(ppv)
    pv = m.group(0)
    result = {
        "implementation_name": implementation_name,
        "implementation_version": implementation_version,
        "os_name": os.name,
        "platform_machine": platform.machine(),
        "platform_python_implementation": platform.python_implementation(),
        "platform_release": platform.release(),
        "platform_system": platform.system(),
        "platform_version": platform.version(),
        "platform_in_venv": str(in_venv()),
        "python_full_version": ppv,
        "python_version": pv,
        "sys_platform": sys.platform,
    }
    return result


DEFAULT_CONTEXT = default_context()
del default_context

evaluator = Evaluator()


def interpret(marker, execution_context=None):
    """
    Interpret a marker and return a result depending on environment.

    :param marker: The marker to interpret.
    :type marker: str
    :param execution_context: The context used for name lookup.
    :type execution_context: mapping
    """
    try:
        expr, rest = parse_marker(marker)
    except Exception as e:
        raise SyntaxError("Unable to interpret marker syntax: %s: %s" % (marker, e))
    if rest and rest[0] != "#":
        raise SyntaxError("unexpected trailing data in marker: %s: %s" % (marker, rest))
    context = dict(DEFAULT_CONTEXT)
    if execution_context:
        context.update(execution_context)
    return evaluator.evaluate(expr, context)
