from yaql.language import exceptions as yaql_exceptions
from yaql.language import expressions
from yaql.language import specs
from yaql.language import utils
from yaql.language import yaqltypes

from murano.dsl import contracts
from murano.dsl import exceptions
from murano.dsl import helpers


class Check(contracts.ContractMethod):
    name = 'check'

    @specs.parameter('predicate', yaqltypes.YaqlExpression())
    @specs.parameter('msg', yaqltypes.String(nullable=True))
    def __init__(self, engine, predicate, msg=None):
        self.engine = engine
        self.predicate = predicate
        self.msg = msg

    def _call_predicate(self, value):
        context = self.root_context.create_child_context()
        context['$'] = value
        return self.predicate(utils.NO_VALUE, context, self.engine)

    def validate(self):
        if isinstance(self.value, contracts.ObjRef) or self._call_predicate(
                self.value):
            return self.value
        else:
            msg = self.msg
            if not msg:
                msg = "Value {0} doesn't match predicate".format(
                    helpers.format_scalar(self.value))
            raise exceptions.ContractViolationException(msg)

    def transform(self):
        return self.validate()

    def generate_schema(self):
        rest = [True]
        while rest:
            if (isinstance(self.predicate, expressions.BinaryOperator) and
                    self.predicate.operator == 'and'):
                rest = self.predicate.args[1]
                self.predicate = self.predicate.args[0]
            else:
                rest = []
            res = extract_pattern(self.predicate, self.engine,
                                  self.root_context)
            if res is not None:
                self.value.update(res)
            self.predicate = rest
        return self.value


def is_dollar(expr):
    """Check $-expressions in YAQL AST"""
    return (isinstance(expr, expressions.GetContextValue) and
            expr.path.value in ('$', '$1'))


def extract_pattern(expr, engine, context):
    """Translation of certain known patterns of check() contract expressions"""
    if isinstance(expr, expressions.BinaryOperator):
        ops = ('>', '<', '>=', '<=')
        if expr.operator in ops:
            op_index = ops.index(expr.operator)
            if is_dollar(expr.args[0]):
                constant = evaluate_constant(expr.args[1], engine, context)
                if constant is None:
                    return None
            elif is_dollar(expr.args[1]):
                constant = evaluate_constant(expr.args[0], engine, context)
                if constant is None:
                    return None
                op_index = -1 - op_index
            else:
                return None
            op = ops[op_index]
            if op == '>':
                return {'minimum': constant, 'exclusiveMinimum': True}
            elif op == '>=':
                return {'minimum': constant, 'exclusiveMinimum': False}
            if op == '<':
                return {'maximum': constant, 'exclusiveMaximum': True}
            elif op == '<=':
                return {'maximum': constant, 'exclusiveMaximum': False}
        elif expr.operator == 'in' and is_dollar(expr.args[0]):
            lst = evaluate_constant(expr.args[1], engine, context)
            if isinstance(lst, tuple):
                return {'enum': list(lst)}

        elif (expr.operator == '.' and is_dollar(expr.args[0]) and
                isinstance(expr.args[1], expressions.Function)):
            func = expr.args[1]
            if func.name == 'matches':
                constant = evaluate_constant(func.args[0], engine, context)
                if constant is not None:
                    return {'pattern': constant}


def evaluate_constant(expr, engine, context):
    """Evaluate yaql expression into constant value if possible"""
    if isinstance(expr, expressions.Constant):
        return expr.value
    context = context.create_child_context()
    trap = utils.create_marker('trap')
    context['$'] = trap

    @specs.parameter('name', yaqltypes.StringConstant())
    @specs.name('#get_context_data')
    def get_context_data(name, context):
        res = context[name]
        if res is trap:
            raise yaql_exceptions.ResolutionError()
        return res

    context.register_function(get_context_data)

    try:
        return expressions.Statement(expr, engine).evaluate(context=context)
    except yaql_exceptions.YaqlException:
        return None
