#    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 sys
import weakref

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

from murano.common import utils
from murano.dsl import constants
from murano.dsl import dsl
from murano.dsl import dsl_types
from murano.dsl import exceptions
from murano.dsl import helpers
from murano.dsl import macros
from murano.dsl import meta
from murano.dsl import typespec
from murano.dsl import virtual_exceptions
from murano.dsl import yaql_integration


LOG = logging.getLogger(__name__)
macros.register()
virtual_exceptions.register()


class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider):
    def __init__(self, declaring_type, name, payload, original_name=None,
                 ephemeral=False):
        self._name = name
        original_name = original_name or name
        self._declaring_type = weakref.ref(declaring_type)
        self._meta_values = None
        self_ref = self if ephemeral else weakref.proxy(self)

        if callable(payload):
            if isinstance(payload, specs.FunctionDefinition):
                self._body = payload
            else:
                self._body = yaql_integration.get_function_definition(
                    payload, self_ref, original_name)
            self._arguments_scheme = None
            self._scope = self._body.meta.get(constants.META_SCOPE)
            if declaring_type.extension_class and any((
                    helpers.inspect_is_static(
                        declaring_type.extension_class, original_name),
                    helpers.inspect_is_classmethod(
                        declaring_type.extension_class, original_name))):
                self._usage = self._body.meta.get(
                    constants.META_USAGE, dsl_types.MethodUsages.Static)
                if self._usage not in dsl_types.MethodUsages.StaticMethods:
                    raise ValueError(
                        'Invalid Usage for static method ' + self.name)
            else:
                self._usage = (self._body.meta.get(constants.META_USAGE) or
                               dsl_types.MethodUsages.Runtime)
                if self._usage not in dsl_types.MethodUsages.InstanceMethods:
                    raise ValueError(
                        'Invalid Usage for instance method ' + self.name)
                self._resolve_usage_and_scope()
            if self._scope is None:
                self._scope = dsl_types.MethodScopes.Session
            if (self._body.name.startswith('#') or
                    self._body.name.startswith('*')):
                raise ValueError(
                    'Import of special yaql functions is forbidden')
            self._meta = meta.MetaData(
                self._body.meta.get(constants.META_MPL_META),
                dsl_types.MetaTargets.Method,
                declaring_type)
        else:
            payload = payload or {}
            self._body = macros.MethodBlock(payload.get('Body'), name)
            self._usage = payload.get(
                'Usage') or dsl_types.MethodUsages.Runtime
            self._scope = payload.get('Scope')
            self._resolve_usage_and_scope()
            if self._scope is None:
                self._scope = dsl_types.MethodScopes.Session
            arguments_scheme = helpers.list_value(payload.get('Arguments'))
            if isinstance(arguments_scheme, dict):
                arguments_scheme = [{key: value} for key, value in
                                    arguments_scheme.items()]
            self._arguments_scheme = collections.OrderedDict()
            seen_varargs = False
            seen_kwargs = False
            args_order_error = False
            for record in arguments_scheme:
                if not isinstance(record, dict) or len(record) > 1:
                    raise exceptions.DslSyntaxError(
                        'Invalid arguments declaration')
                name = list(record.keys())[0]
                argument = MuranoMethodArgument(
                    self, self.name, name, record[name])
                usage = argument.usage
                if (usage == dsl_types.MethodArgumentUsages.Standard and
                        (seen_kwargs or seen_varargs)):
                    args_order_error = True
                elif usage == dsl_types.MethodArgumentUsages.VarArgs:
                    if seen_kwargs or seen_varargs:
                        args_order_error = True
                    seen_varargs = True
                elif usage == dsl_types.MethodArgumentUsages.KwArgs:
                    if seen_kwargs:
                        args_order_error = True
                    seen_kwargs = True

                if args_order_error:
                    raise exceptions.DslSyntaxError(
                        'Invalid argument order in method {0}'.format(
                            self.name))
                else:
                    self._arguments_scheme[name] = argument

            self._meta = meta.MetaData(
                payload.get('Meta'),
                dsl_types.MetaTargets.Method,
                declaring_type)

        self._instance_stub, self._static_stub = \
            yaql_integration.build_stub_function_definitions(self_ref)

    def _resolve_usage_and_scope(self):
        if self._usage == dsl_types.MethodUsages.Action:
            runtime_version = self.declaring_type.package.runtime_version
            if runtime_version > constants.RUNTIME_VERSION_1_3:
                LOG.warning('"Usage: Action" is deprecated, '
                            'use "Scope: Public" instead')
            if self._scope == dsl_types.MethodScopes.Session:
                raise ValueError(
                    'Both "Usage: Action" and "Scope: Session" are '
                    'provided for method ' + self.name)
            self._scope = dsl_types.MethodScopes.Public

    @property
    def name(self):
        return self._name

    @property
    def declaring_type(self):
        return self._declaring_type()

    @property
    def arguments_scheme(self):
        return self._arguments_scheme

    @property
    def instance_stub(self):
        return self._instance_stub

    @property
    def static_stub(self):
        return self._static_stub

    @property
    def usage(self):
        return self._usage

    @usage.setter
    def usage(self, value):
        self._usage = value

    @property
    def scope(self):
        return self._scope

    @scope.setter
    def scope(self, value):
        self._scope = value

    @property
    def body(self):
        return self._body

    @property
    def is_static(self):
        return self.usage in dsl_types.MethodUsages.StaticMethods

    @property
    def is_action(self):
        return (self.scope == dsl_types.MethodScopes.Public or
                self.usage == dsl_types.MethodUsages.Action)

    def get_meta(self, context):
        def meta_producer(cls):
            method = cls.methods.get(self.name)
            if method is None:
                return None
            return method._meta

        if self._meta_values is None:
            executor = helpers.get_executor()
            context = executor.create_type_context(
                self.declaring_type, caller_context=context)
            self._meta_values = meta.merge_providers(
                self.declaring_type, meta_producer, context)
        return self._meta_values

    def __repr__(self):
        return 'MuranoMethod({0}::{1})'.format(
            self.declaring_type.name, self.name)

    def invoke(self, this, args, kwargs, context=None, skip_stub=False):
        if isinstance(this, dsl.MuranoObjectInterface):
            this = this.object
        if this and not self.declaring_type.is_compatible(this):
            raise Exception("'this' must be of compatible type")
        if not this and not self.is_static:
            raise Exception("A class instance is required")

        if isinstance(this, dsl_types.MuranoObject):
            this = this.cast(self.declaring_type)
        else:
            this = self.declaring_type
        executor = helpers.get_executor()
        return executor.invoke_method(
            self, this, context, args, kwargs, skip_stub)


class MuranoMethodArgument(dsl_types.MuranoMethodArgument, typespec.Spec,
                           meta.MetaProvider):
    def __init__(self, murano_method, method_name, arg_name, declaration):
        super(MuranoMethodArgument, self).__init__(
            declaration, murano_method.declaring_type)
        self._method_name = method_name
        self._arg_name = arg_name
        self._murano_method = weakref.ref(murano_method)
        self._meta = meta.MetaData(
            declaration.get('Meta'),
            dsl_types.MetaTargets.Argument, self.murano_method.declaring_type)
        self._usage = declaration.get('Usage') or \
            dsl_types.MethodArgumentUsages.Standard

        if self._usage not in dsl_types.MethodArgumentUsages.All:
            raise exceptions.DslSyntaxError(
                'Unknown usage {0}. Must be one of ({1})'.format(
                    self._usage, ', '.join(dsl_types.MethodArgumentUsages.All)
                ))

    def transform(self, value, this, *args, **kwargs):
        try:
            if self.murano_method.usage == dsl_types.MethodUsages.Extension:
                this = self.murano_method.declaring_type
            return super(MuranoMethodArgument, self).transform(
                value, this, *args, **kwargs)
        except exceptions.ContractViolationException as e:
            msg = u'[{0}::{1}({2}{3})] {4}'.format(
                self.murano_method.declaring_type.name,
                self.murano_method.name, self.name,
                e.path, str(e))
            utils.reraise(exceptions.ContractViolationException,
                          exceptions.ContractViolationException(msg),
                          sys.exc_info()[2])

    @property
    def murano_method(self):
        return self._murano_method()

    @property
    def name(self):
        return self._arg_name

    @property
    def usage(self):
        return self._usage

    def get_meta(self, context):
        executor = helpers.get_executor()
        context = executor.create_type_context(
            self.murano_method.declaring_type, caller_context=context)

        return self._meta.get_meta(context)

    def __repr__(self):
        return 'MuranoMethodArgument({method}::{name})'.format(
            method=self.murano_method.name, name=self.name)
