"""
Parser and utilities for the smart 'if' tag
"""
import warnings

from django.utils.deprecation import RemovedInDjango110Warning


# Using a simple top down parser, as described here:
#    http://effbot.org/zone/simple-top-down-parsing.htm.
# 'led' = left denotation
# 'nud' = null denotation
# 'bp' = binding power (left = lbp, right = rbp)

class TokenBase(object):
    """
    Base class for operators and literals, mainly for debugging and for throwing
    syntax errors.
    """
    id = None  # node/token type name
    value = None  # used by literals
    first = second = None  # used by tree nodes

    def nud(self, parser):
        # Null denotation - called in prefix context
        raise parser.error_class(
            "Not expecting '%s' in this position in if tag." % self.id
        )

    def led(self, left, parser):
        # Left denotation - called in infix context
        raise parser.error_class(
            "Not expecting '%s' as infix operator in if tag." % self.id
        )

    def display(self):
        """
        Returns what to display in error messages for this node
        """
        return self.id

    def __repr__(self):
        out = [str(x) for x in [self.id, self.first, self.second] if x is not None]
        return "(" + " ".join(out) + ")"


def infix(bp, func):
    """
    Creates an infix operator, given a binding power and a function that
    evaluates the node
    """
    class Operator(TokenBase):
        lbp = bp

        def led(self, left, parser):
            self.first = left
            self.second = parser.expression(bp)
            return self

        def eval(self, context):
            try:
                return func(context, self.first, self.second)
            except Exception:
                # Templates shouldn't throw exceptions when rendering.  We are
                # most likely to get exceptions for things like {% if foo in bar
                # %} where 'bar' does not support 'in', so default to False
                return False

    return Operator


def prefix(bp, func):
    """
    Creates a prefix operator, given a binding power and a function that
    evaluates the node.
    """
    class Operator(TokenBase):
        lbp = bp

        def nud(self, parser):
            self.first = parser.expression(bp)
            self.second = None
            return self

        def eval(self, context):
            try:
                return func(context, self.first)
            except Exception:
                return False

    return Operator


# Operator precedence follows Python.
# NB - we can get slightly more accurate syntax error messages by not using the
# same object for '==' and '='.
# We defer variable evaluation to the lambda to ensure that terms are
# lazily evaluated using Python's boolean parsing logic.
OPERATORS = {
    'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)),
    'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)),
    'not': prefix(8, lambda context, x: not x.eval(context)),
    'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)),
    'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)),
    # This should be removed in Django 1.10:
    '=': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
    '==': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
    '!=': infix(10, lambda context, x, y: x.eval(context) != y.eval(context)),
    '>': infix(10, lambda context, x, y: x.eval(context) > y.eval(context)),
    '>=': infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)),
    '<': infix(10, lambda context, x, y: x.eval(context) < y.eval(context)),
    '<=': infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)),
}

# Assign 'id' to each:
for key, op in OPERATORS.items():
    op.id = key


class Literal(TokenBase):
    """
    A basic self-resolvable object similar to a Django template variable.
    """
    # IfParser uses Literal in create_var, but TemplateIfParser overrides
    # create_var so that a proper implementation that actually resolves
    # variables, filters etc. is used.
    id = "literal"
    lbp = 0

    def __init__(self, value):
        self.value = value

    def display(self):
        return repr(self.value)

    def nud(self, parser):
        return self

    def eval(self, context):
        return self.value

    def __repr__(self):
        return "(%s %r)" % (self.id, self.value)


class EndToken(TokenBase):
    lbp = 0

    def nud(self, parser):
        raise parser.error_class("Unexpected end of expression in if tag.")

EndToken = EndToken()


class IfParser(object):
    error_class = ValueError

    def __init__(self, tokens):
        # pre-pass necessary to turn  'not','in' into single token
        l = len(tokens)
        mapped_tokens = []
        i = 0
        while i < l:
            token = tokens[i]
            if token == "not" and i + 1 < l and tokens[i + 1] == "in":
                token = "not in"
                i += 1  # skip 'in'
            mapped_tokens.append(self.translate_token(token))
            i += 1

        self.tokens = mapped_tokens
        self.pos = 0
        self.current_token = self.next_token()

    def translate_token(self, token):
        try:
            op = OPERATORS[token]
        except (KeyError, TypeError):
            return self.create_var(token)
        else:
            if token == '=':
                warnings.warn(
                    "Operator '=' is deprecated and will be removed in Django 1.10. Use '==' instead.",
                    RemovedInDjango110Warning, stacklevel=2
                )
            return op()

    def next_token(self):
        if self.pos >= len(self.tokens):
            return EndToken
        else:
            retval = self.tokens[self.pos]
            self.pos += 1
            return retval

    def parse(self):
        retval = self.expression()
        # Check that we have exhausted all the tokens
        if self.current_token is not EndToken:
            raise self.error_class("Unused '%s' at end of if expression." %
                                   self.current_token.display())
        return retval

    def expression(self, rbp=0):
        t = self.current_token
        self.current_token = self.next_token()
        left = t.nud(self)
        while rbp < self.current_token.lbp:
            t = self.current_token
            self.current_token = self.next_token()
            left = t.led(left, self)
        return left

    def create_var(self, value):
        return Literal(value)
