import ast

from .. import validators as val

safe_globals = ('True', 'False', 'None')
safe_builtins = dict((f, __builtins__[f]) for f in safe_globals)


def _validate_expr(call_node, validators):
    # Validate that the expression uses a known, registered validator.
    try:
        func_name = call_node.func.id
    except AttributeError:
        raise SyntaxError('Schema expressions must be enclosed by a validator.')
    if func_name not in validators:
        raise SyntaxError('Not a registered validator: \'%s\'. ' % func_name)
    # Validate that all args are constant literals, validator names,  or other call nodes
    arg_values = call_node.args + [kw.value for kw in call_node.keywords]
    for arg in arg_values:
        base_arg = arg.operand if isinstance(arg, ast.UnaryOp) else arg
        if isinstance(base_arg, ast.Constant):
            continue
        elif isinstance(base_arg, ast.Name) and base_arg.id in validators:
            continue
        elif isinstance(base_arg, ast.Call):
            _validate_expr(base_arg, validators)
        else:
            raise SyntaxError(
                'Argument values must either be constant literals, or else '
                'reference other validators.')


def parse(validator_string, validators=None):
    validators = validators or val.DefaultValidators
    try:
        tree = ast.parse(validator_string, mode='eval')
        _validate_expr(tree.body, validators)
        # evaluate with access to a limited global scope only
        return eval(compile(tree, '<ast>', 'eval'),
                    {'__builtins__': safe_builtins},
                    validators)
    except (SyntaxError, NameError, TypeError) as e:
        raise SyntaxError(
            'Invalid schema expression: \'%s\'. ' % validator_string +
            str(e)
        )
