from __future__ import absolute_import
import six
import ast

from spidermon.exceptions import InvalidExpression


class Interpreter(object):

    ast_allowed_nodes = (
        'expr', 'name', 'load', 'call', 'store',
        'str', 'unicode', 'num', 'list', 'dict', 'set', 'tuple',  # Data types
        'nameconstant',  # language constant (None)
        'unaryop', 'usub',  # Unary arithmetic operators
        'binop', 'add', 'sub', 'div', 'mult', 'mod', 'pow', 'floordiv',  # Binary arithmetic operators
        'compare', 'eq', 'noteq', 'gt', 'lt', 'gte', 'lte',  # Comparison operators
        'bitand', 'bitor', 'bitxor', 'invert', 'lshift', 'rshift',  # Bitwise operators
        'boolop', 'and', 'or', 'not',  # Logical operators
        'in', 'notin',  # Membership operators
        'is', 'isnot',  # Identity operators
        'ifexp',  # Inline if statement
        'subscript', 'index', 'slice', 'extslice',  # Subscripting
        'listcomp', 'setcomp', 'dictcomp',  'generatorexp', 'comprehension',  # Comprehensions
        'attribute',  # Attribute access
    )

    allowed_objects = (
        str, six.text_type,  # strings
        int, float, complex,  # numbers
        list, dict, set, tuple,  # sequences
        type(None), bool  # others
    )
    try:
        # Py2 compatibility
        allowed_objects += (long,)
    except NameError:
        pass

    def check(self, expression):
        if not isinstance(expression, six.string_types):
            raise InvalidExpression('Python expressions must be defined as strings')
        if not expression:
            raise InvalidExpression('Empty python expression')

        try:
            tree = ast.parse(expression)
        except SyntaxError as e:
            raise e

        if not tree.body:
            raise InvalidExpression('Empty python expression')
        elif len(tree.body) > 1:
            raise InvalidExpression('Python expressions must be a single line expression')

        start_node = tree.body[0]
        if not isinstance(start_node, ast.Expr):
            raise InvalidExpression("Python string must be an expression: '%s' found" %
                                    start_node.__class__.__name__)

        self._check_node(start_node)

    def eval(self, expression, context=None, check=True):
        if check:
            self.check(expression)
        return eval(expression, context)

    def _check_node(self, node):
        if isinstance(node, list):
            self._check_node_list(node)
        elif isinstance(node, ast.AST):
            if not self._is_allowed_ast_node(node):
                self._raise_not_allowed_node(node)
            self._check_node_fields(node)
        elif not isinstance(node, self.allowed_objects):
            self._raise_not_allowed_node(node)

    def _check_node_list(self, node_list):
        for node in node_list:
            self._check_node(node)

    def _check_node_fields(self, node):
        for field in [f for _, f in ast.iter_fields(node)]:
            self._check_node(field)

    def _is_allowed_ast_node(self, node):
        return node.__class__.__name__.lower() in self.ast_allowed_nodes

    def _raise_not_allowed_node(self, node):
        raise InvalidExpression("'%s' definition not allowed in python expressions" %
                                node.__class__.__name__)
