#    Copyright (c) 2014 Mirantis, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import types

import eventlet.greenpool as greenpool
import yaql.context

import murano.dsl.dsl_exception as dsl_exception
import murano.dsl.exceptions as exceptions
import murano.dsl.expressions as expressions
import murano.dsl.helpers as helpers
import murano.dsl.yaql_expression as yaql_expression


class CodeBlock(expressions.DslExpression):
    def __init__(self, body):
        if not isinstance(body, types.ListType):
            body = [body]
        self.code_block = map(expressions.parse_expression, body)

    def execute(self, context, murano_class):
        for expr in self.code_block:
            def action():
                try:
                    expr.execute(context, murano_class)
                except (dsl_exception.MuranoPlException,
                        exceptions.InternalFlowException):
                    raise
                except Exception as ex:
                    raise dsl_exception.MuranoPlException.\
                        from_python_exception(ex, context)

            if hasattr(expr, 'virtual_instruction'):
                instruction = expr.virtual_instruction
                helpers.execute_instruction(instruction, action, context)
            else:
                action()


class MethodBlock(CodeBlock):
    def __init__(self, body, name=None):
        super(MethodBlock, self).__init__(body)
        self._name = name

    def execute(self, context, murano_class):
        new_context = yaql.context.Context(context)
        new_context.set_data(self._name, '?currentMethod')
        try:
            super(MethodBlock, self).execute(new_context, murano_class)
        except exceptions.ReturnException as e:
            return e.value
        except exceptions.BreakException:
            raise exceptions.DslInvalidOperationError(
                'Break cannot be used on method level')
        except exceptions.ContinueException:
            raise exceptions.DslInvalidOperationError(
                'Continue cannot be used on method level')
        else:
            return None


class ReturnMacro(expressions.DslExpression):
    def __init__(self, Return):
        self._value = Return

    def execute(self, context, murano_class):
        raise exceptions.ReturnException(
            helpers.evaluate(self._value, context))


class BreakMacro(expressions.DslExpression):
    def __init__(self, Break):
        if Break:
            raise exceptions.DslSyntaxError('Break cannot have value')

    def execute(self, context, murano_class):
        raise exceptions.BreakException()


class ContinueMacro(expressions.DslExpression):
    def __init__(self, Continue):
        if Continue:
            raise exceptions.DslSyntaxError('Continue cannot have value')

    def execute(self, context, murano_class):
        raise exceptions.ContinueException()


class ParallelMacro(CodeBlock):
    def __init__(self, Parallel, Limit=None):
        super(ParallelMacro, self).__init__(Parallel)
        if Limit:
            self._limit = yaql_expression.YaqlExpression(str(Limit))
        else:
            self._limit = len(self.code_block)

    def execute(self, context, murano_class):
        if not self.code_block:
            return
        limit = helpers.evaluate(self._limit, context)
        gpool = greenpool.GreenPool(helpers.evaluate(limit, context))
        for expr in self.code_block:
            gpool.spawn_n(expr.execute, context, murano_class)
        gpool.waitall()


class IfMacro(expressions.DslExpression):
    def __init__(self, If, Then, Else=None):
        if not isinstance(If, yaql_expression.YaqlExpression):
            raise exceptions.DslSyntaxError(
                'Condition must be of expression type')
        self._code1 = CodeBlock(Then)
        self._code2 = None if Else is None else CodeBlock(Else)
        self._condition = If

    def execute(self, context, murano_class):
        res = self._condition.evaluate(context)
        if not isinstance(res, types.BooleanType):
            raise exceptions.DslInvalidOperationError(
                'Condition must be evaluated to boolean type')
        if res:
            self._code1.execute(context, murano_class)
        elif self._code2 is not None:
            self._code2.execute(context, murano_class)


class WhileDoMacro(expressions.DslExpression):
    def __init__(self, While, Do):
        if not isinstance(While, yaql_expression.YaqlExpression):
            raise TypeError()
        self._code = CodeBlock(Do)
        self._condition = While

    def execute(self, context, murano_class):
        while True:
            res = self._condition.evaluate(context)
            if not isinstance(res, types.BooleanType):
                raise exceptions.DslSyntaxError(
                    'Condition must be of expression type')
            try:
                if res:
                    self._code.execute(context, murano_class)
                else:
                    break
            except exceptions.BreakException:
                break
            except exceptions.ContinueException:
                continue


class ForMacro(expressions.DslExpression):
    def __init__(self, For, In, Do):
        if not isinstance(For, types.StringTypes):
            raise exceptions.DslSyntaxError(
                'For value must be of string type')
        self._code = CodeBlock(Do)
        self._var = For
        self._collection = In

    def execute(self, context, murano_class):
        collection = helpers.evaluate(self._collection, context)
        for t in collection:
            context.set_data(t, self._var)
            try:
                self._code.execute(context, murano_class)
            except exceptions.BreakException:
                break
            except exceptions.ContinueException:
                continue


class RepeatMacro(expressions.DslExpression):
    def __init__(self, Repeat, Do):
        if not isinstance(Repeat, (int, yaql_expression.YaqlExpression)):
            raise exceptions.DslSyntaxError(
                'Repeat value must be either int or expression')
        self._count = Repeat
        self._code = CodeBlock(Do)

    def execute(self, context, murano_class):
        count = helpers.evaluate(self._count, context)
        for t in range(0, count):
            try:
                self._code.execute(context, murano_class)
            except exceptions.BreakException:
                break
            except exceptions.ContinueException:
                continue


class MatchMacro(expressions.DslExpression):
    def __init__(self, Match, Value, Default=None):
        if not isinstance(Match, types.DictionaryType):
            raise exceptions.DslSyntaxError(
                'Match value must be of dictionary type')
        self._switch = Match
        self._value = Value
        self._default = None if Default is None else CodeBlock(Default)

    def execute(self, context, murano_class):
        match_value = helpers.evaluate(self._value, context)
        for key, value in self._switch.iteritems():
            if key == match_value:
                CodeBlock(value).execute(context, murano_class)
                return
        if self._default is not None:
            self._default.execute(context, murano_class)


class SwitchMacro(expressions.DslExpression):
    def __init__(self, Switch, Default=None):
        if not isinstance(Switch, types.DictionaryType):
            raise exceptions.DslSyntaxError(
                'Switch value must be of dictionary type')
        self._switch = Switch
        for key, value in self._switch.iteritems():
            if not isinstance(key, (yaql_expression.YaqlExpression,
                                    types.BooleanType)):
                raise exceptions.DslSyntaxError(
                    'Switch cases must be must be either '
                    'boolean or expression')
        self._default = None if Default is None else CodeBlock(Default)

    def execute(self, context, murano_class):
        matched = False
        for key, value in self._switch.iteritems():
            res = helpers.evaluate(key, context)
            if not isinstance(res, types.BooleanType):
                raise exceptions.DslInvalidOperationError(
                    'Switch case must be evaluated to boolean type')
            if res:
                matched = True
                CodeBlock(value).execute(context, murano_class)

        if self._default is not None and not matched:
            self._default.execute(context, murano_class)


class DoMacro(expressions.DslExpression):
    def __init__(self, Do):
        self._code = CodeBlock(Do)

    def execute(self, context, murano_class):
        self._code.execute(context, murano_class)


def register():
    expressions.register_macro(DoMacro)
    expressions.register_macro(ReturnMacro)
    expressions.register_macro(BreakMacro)
    expressions.register_macro(ContinueMacro)
    expressions.register_macro(ParallelMacro)
    expressions.register_macro(IfMacro)
    expressions.register_macro(WhileDoMacro)
    expressions.register_macro(ForMacro)
    expressions.register_macro(RepeatMacro)
    expressions.register_macro(MatchMacro)
    expressions.register_macro(SwitchMacro)
