"""Parser for the environment markers micro-language defined in PEP 345."""

import os
import sys
import platform
from io import BytesIO
from tokenize import tokenize, NAME, OP, STRING, ENDMARKER, ENCODING

__all__ = ['interpret']


# allowed operators
_OPERATORS = {'==': lambda x, y: x == y,
              '!=': lambda x, y: x != y,
              '>': lambda x, y: x > y,
              '>=': lambda x, y: x >= y,
              '<': lambda x, y: x < y,
              '<=': lambda x, y: x <= y,
              'in': lambda x, y: x in y,
              'not in': lambda x, y: x not in y}


def _operate(operation, x, y):
    return _OPERATORS[operation](x, y)


# restricted set of variables
_VARS = {'sys.platform': sys.platform,
         'python_version': '%s.%s' % sys.version_info[:2],
         # FIXME parsing sys.platform is not reliable, but there is no other
         # way to get e.g. 2.7.2+, and the PEP is defined with sys.version
         'python_full_version': sys.version.split(' ', 1)[0],
         'os.name': os.name,
         'platform.version': platform.version(),
         'platform.machine': platform.machine(),
         'platform.python_implementation': platform.python_implementation(),
        }


class _Operation:

    def __init__(self, execution_context=None):
        self.left = None
        self.op = None
        self.right = None
        if execution_context is None:
            execution_context = {}
        self.execution_context = execution_context

    def _get_var(self, name):
        if name in self.execution_context:
            return self.execution_context[name]
        return _VARS[name]

    def __repr__(self):
        return '%s %s %s' % (self.left, self.op, self.right)

    def _is_string(self, value):
        if value is None or len(value) < 2:
            return False
        for delimiter in '"\'':
            if value[0] == value[-1] == delimiter:
                return True
        return False

    def _is_name(self, value):
        return value in _VARS

    def _convert(self, value):
        if value in _VARS:
            return self._get_var(value)
        return value.strip('"\'')

    def _check_name(self, value):
        if value not in _VARS:
            raise NameError(value)

    def _nonsense_op(self):
        msg = 'This operation is not supported : "%s"' % self
        raise SyntaxError(msg)

    def __call__(self):
        # make sure we do something useful
        if self._is_string(self.left):
            if self._is_string(self.right):
                self._nonsense_op()
            self._check_name(self.right)
        else:
            if not self._is_string(self.right):
                self._nonsense_op()
            self._check_name(self.left)

        if self.op not in _OPERATORS:
            raise TypeError('Operator not supported "%s"' % self.op)

        left = self._convert(self.left)
        right = self._convert(self.right)
        return _operate(self.op, left, right)


class _OR:
    def __init__(self, left, right=None):
        self.left = left
        self.right = right

    def filled(self):
        return self.right is not None

    def __repr__(self):
        return 'OR(%r, %r)' % (self.left, self.right)

    def __call__(self):
        return self.left() or self.right()


class _AND:
    def __init__(self, left, right=None):
        self.left = left
        self.right = right

    def filled(self):
        return self.right is not None

    def __repr__(self):
        return 'AND(%r, %r)' % (self.left, self.right)

    def __call__(self):
        return self.left() and self.right()


def interpret(marker, execution_context=None):
    """Interpret a marker and return a result depending on environment."""
    marker = marker.strip().encode()
    ops = []
    op_starting = True
    for token in tokenize(BytesIO(marker).readline):
        # Unpack token
        toktype, tokval, rowcol, line, logical_line = token
        if toktype not in (NAME, OP, STRING, ENDMARKER, ENCODING):
            raise SyntaxError('Type not supported "%s"' % tokval)

        if op_starting:
            op = _Operation(execution_context)
            if len(ops) > 0:
                last = ops[-1]
                if isinstance(last, (_OR, _AND)) and not last.filled():
                    last.right = op
                else:
                    ops.append(op)
            else:
                ops.append(op)
            op_starting = False
        else:
            op = ops[-1]

        if (toktype == ENDMARKER or
            (toktype == NAME and tokval in ('and', 'or'))):
            if toktype == NAME and tokval == 'and':
                ops.append(_AND(ops.pop()))
            elif toktype == NAME and tokval == 'or':
                ops.append(_OR(ops.pop()))
            op_starting = True
            continue

        if isinstance(op, (_OR, _AND)) and op.right is not None:
            op = op.right

        if ((toktype in (NAME, STRING) and tokval not in ('in', 'not'))
            or (toktype == OP and tokval == '.')):
            if op.op is None:
                if op.left is None:
                    op.left = tokval
                else:
                    op.left += tokval
            else:
                if op.right is None:
                    op.right = tokval
                else:
                    op.right += tokval
        elif toktype == OP or tokval in ('in', 'not'):
            if tokval == 'in' and op.op == 'not':
                op.op = 'not in'
            else:
                op.op = tokval

    for op in ops:
        if not op():
            return False
    return True
