#    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 collections
import contextlib
import itertools
import types
import weakref

import eventlet
import eventlet.event
from oslo_log import log as logging
from yaql.language import specs

from murano.common.i18n import _LW
from murano.dsl import attribute_store
from murano.dsl import constants
from murano.dsl import dsl
from murano.dsl import helpers
from murano.dsl import linked_context
from murano.dsl import murano_method
from murano.dsl import object_store
from murano.dsl.principal_objects import stack_trace
from murano.dsl import yaql_integration

LOG = logging.getLogger(__name__)


class MuranoDslExecutor(object):
    def __init__(self, package_loader, context_manager, environment=None):
        self._package_loader = package_loader
        self._context_manager = context_manager
        self._environment = environment
        self._attribute_store = attribute_store.AttributeStore()
        self._object_store = object_store.ObjectStore(self)
        self._locks = {}
        self._root_context_cache = {}

    @property
    def object_store(self):
        return self._object_store

    @property
    def attribute_store(self):
        return self._attribute_store

    @property
    def package_loader(self):
        return self._package_loader

    @property
    def context_manager(self):
        return self._context_manager

    def invoke_method(self, method, this, context, args, kwargs,
                      skip_stub=False):
        if isinstance(this, dsl.MuranoObjectInterface):
            this = this.object
        kwargs = helpers.filter_parameters_dict(kwargs)
        runtime_version = method.murano_class.package.runtime_version
        yaql_engine = yaql_integration.choose_yaql_engine(runtime_version)
        if context is None or not skip_stub:
            actions_only = context is None and not method.name.startswith('.')
            method_context = self.create_method_context(
                self.create_object_context(this, context), method)
            method_context[constants.CTX_SKIP_FRAME] = True
            method_context[constants.CTX_ACTIONS_ONLY] = actions_only
            return method.yaql_function_definition(
                yaql_engine, method_context, this.real_this)(*args, **kwargs)

        if (context[constants.CTX_ACTIONS_ONLY] and method.usage !=
                murano_method.MethodUsages.Action):
            raise Exception('{0} is not an action'.format(method.name))

        context = self.create_method_context(
            self.create_object_context(this, context), method)
        this = this.real_this

        if method.arguments_scheme is not None:
            args, kwargs = self._canonize_parameters(
                method.arguments_scheme, args, kwargs)

        with self._acquire_method_lock(method, this):
            for i, arg in enumerate(args, 2):
                context[str(i)] = arg
            for key, value in kwargs.iteritems():
                context[key] = value

            def call():
                if isinstance(method.body, specs.FunctionDefinition):
                    native_this = this.cast(
                        method.murano_class).extension
                    return method.body(
                        yaql_engine, context, native_this)(*args, **kwargs)
                else:
                    return (None if method.body is None
                            else method.body.execute(context))

            if (not isinstance(method.body, specs.FunctionDefinition)
                    or not method.body.meta.get(constants.META_NO_TRACE)):
                with self._log_method(context, args, kwargs) as log:
                    result = call()
                    log(result)
                    return result
            else:
                return call()

    @contextlib.contextmanager
    def _acquire_method_lock(self, func, this):
        method_id = id(func)
        this_id = this.object_id
        thread_id = helpers.get_current_thread_id()
        while True:
            event, event_owner = self._locks.get(
                (method_id, this_id), (None, None))
            if event:
                if event_owner == thread_id:
                    event = None
                    break
                else:
                    event.wait()
            else:
                event = eventlet.event.Event()
                self._locks[(method_id, this_id)] = (event, thread_id)
                break
        try:
            yield
        finally:
            if event is not None:
                del self._locks[(method_id, this_id)]
                event.send()

    @contextlib.contextmanager
    def _log_method(self, context, args, kwargs):
        method = helpers.get_current_method(context)
        param_gen = itertools.chain(
            (unicode(arg) for arg in args),
            (u'{0} => {1}'.format(name, value)
             for name, value in kwargs.iteritems()))
        params_str = u', '.join(param_gen)
        method_name = '{0}::{1}'.format(method.murano_class.name, method.name)
        thread_id = helpers.get_current_thread_id()
        caller_str = ''
        caller_ctx = helpers.get_caller_context(context)
        if caller_ctx is not None:
            frame = stack_trace.compose_stack_frame(caller_ctx)
            if frame['location']:
                caller_str = ' called from ' + stack_trace.format_frame(frame)

        LOG.trace(u'{thread}: Begin execution {method}({params}){caller}'
                  .format(thread=thread_id, method=method_name,
                          params=params_str, caller=caller_str))
        try:
            def log_result(result):
                LOG.trace(
                    u'{thread}: End execution {method} with result '
                    u'{result}'.format(
                        thread=thread_id, method=method_name, result=result))
            yield log_result
        except Exception as e:
            LOG.trace(
                u'{thread}: End execution {method} with exception '
                u'{exc}'.format(thread=thread_id, method=method_name, exc=e))
            raise

    @staticmethod
    def _canonize_parameters(arguments_scheme, args, kwargs):
        arg_names = arguments_scheme.keys()
        parameter_values = helpers.filter_parameters_dict(kwargs)
        for i, arg in enumerate(args):
            name = arg_names[i]
            parameter_values[name] = arg
        return tuple(), parameter_values

    def load(self, data):
        if not isinstance(data, types.DictionaryType):
            raise TypeError()
        self._attribute_store.load(data.get(constants.DM_ATTRIBUTES) or [])
        result = self._object_store.load(data.get(constants.DM_OBJECTS), None)
        if result is None:
            return None
        return dsl.MuranoObjectInterface(result, executor=self)

    def cleanup(self, data):
        objects_copy = data.get(constants.DM_OBJECTS_COPY)
        if not objects_copy:
            return
        gc_object_store = object_store.ObjectStore(self)
        gc_object_store.load(objects_copy, None)
        objects_to_clean = []
        for object_id in self._list_potential_object_ids(objects_copy):
            if (gc_object_store.has(object_id)
                    and not self._object_store.has(object_id)):
                obj = gc_object_store.get(object_id)
                objects_to_clean.append(obj)
        if objects_to_clean:
            for obj in objects_to_clean:
                methods = obj.type.find_methods(lambda m: m.name == '.destroy')
                for method in methods:
                    try:
                        method.invoke(self, obj, (), {}, None)
                    except Exception as e:
                        LOG.warn(_LW(
                            'Muted exception during execution of .destroy '
                            'on {0}: {1}').format(obj, e), exc_info=True)

    def _list_potential_object_ids(self, data):
        if isinstance(data, types.DictionaryType):
            for val in data.values():
                for res in self._list_potential_object_ids(val):
                    yield res
            sys_dict = data.get('?')
            if (isinstance(sys_dict, types.DictionaryType)
                    and sys_dict.get('id')
                    and sys_dict.get('type')):
                yield sys_dict['id']
        elif isinstance(data, collections.Iterable) and not isinstance(
                data, types.StringTypes):
            for val in data:
                for res in self._list_potential_object_ids(val):
                    yield res

    def create_root_context(self, runtime_version):
        context = self._root_context_cache.get(runtime_version)
        if not context:
            context = self.context_manager.create_root_context(runtime_version)
            context = context.create_child_context()
            context[constants.CTX_EXECUTOR] = weakref.ref(self)
            context[constants.CTX_PACKAGE_LOADER] = weakref.ref(
                self._package_loader)
            context[constants.CTX_ENVIRONMENT] = self._environment
            context[constants.CTX_ATTRIBUTE_STORE] = weakref.ref(
                self._attribute_store)
            self._root_context_cache[runtime_version] = context
        return context

    def create_package_context(self, package):
        root_context = self.create_root_context(package.runtime_version)
        context = linked_context.link(
            root_context,
            self.context_manager.create_package_context(package))
        return context

    def create_class_context(self, murano_class):
        package_context = self.create_package_context(
            murano_class.package)
        context = linked_context.link(
            package_context,
            self.context_manager.create_class_context(
                murano_class)).create_child_context()
        context[constants.CTX_TYPE] = murano_class
        return context

    def create_object_context(self, obj, caller_context=None):
        class_context = self.create_class_context(obj.type)
        context = linked_context.link(
            class_context, self.context_manager.create_object_context(
                obj)).create_child_context()
        context[constants.CTX_THIS] = obj.real_this
        context['this'] = obj.real_this
        context[''] = obj.real_this

        if caller_context is not None:
            caller = caller_context
            while caller is not None and caller[constants.CTX_SKIP_FRAME]:
                caller = caller[constants.CTX_CALLER_CONTEXT]
            context[constants.CTX_CALLER_CONTEXT] = caller
            context[constants.CTX_ALLOW_PROPERTY_WRITES] = caller_context[
                constants.CTX_ALLOW_PROPERTY_WRITES]
        return context

    @staticmethod
    def create_method_context(object_context, method):
        context = object_context.create_child_context()
        context[constants.CTX_CURRENT_METHOD] = method
        return context
