#    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 inspect
import warnings
import weakref

import debtcollector
import semantic_version
import six
from yaql.language import specs
from yaql.language import utils

from murano.dsl import constants
from murano.dsl import dsl_types
from murano.dsl import exceptions
from murano.dsl import helpers
from murano.dsl import meta as dslmeta
from murano.dsl import murano_object
from murano.dsl import murano_type
from murano.dsl import namespace_resolver
from murano.dsl import principal_objects
from murano.dsl import yaql_integration


class MuranoPackage(dsl_types.MuranoPackage, dslmeta.MetaProvider):
    def __init__(self, package_loader, name, version=None,
                 runtime_version=None, requirements=None, meta=None):
        super(MuranoPackage, self).__init__()
        self._package_loader = weakref.proxy(package_loader)
        self._name = name
        self._version = helpers.parse_version(version)
        self._runtime_version = helpers.parse_version(runtime_version)
        self._requirements = {
            name: semantic_version.Spec('==' + str(self._version.major))
        }
        if name != constants.CORE_LIBRARY:
            self._requirements[constants.CORE_LIBRARY] = \
                semantic_version.Spec('==0')
        self._classes = {}
        self._imported_types = {object, murano_object.MuranoObject}
        for key, value in six.iteritems(requirements or {}):
            self._requirements[key] = helpers.parse_version_spec(value)

        self._load_queue = {}
        self._native_load_queue = {}
        if self.name == constants.CORE_LIBRARY:
            principal_objects.register(self)
        self._package_class = self._create_package_class()
        self._meta = lambda: dslmeta.MetaData(
            meta, dsl_types.MetaTargets.Package, self._package_class)

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

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

    @property
    def version(self):
        return self._version

    @property
    def runtime_version(self):
        return self._runtime_version

    @property
    def requirements(self):
        return self._requirements

    @property
    def classes(self):
        return set(self._classes.keys()).union(
            self._load_queue.keys()).union(self._native_load_queue.keys())

    def get_resource(self, name):
        raise NotImplementedError('resource API is not implemented')

    # noinspection PyMethodMayBeStatic
    def get_class_config(self, name):
        return {}

    def _register_mpl_classes(self, data, name=None):
        type_obj = self._classes.get(name)
        if type_obj is not None:
            return type_obj
        if callable(data):
            data = data()
        data = helpers.list_value(data)
        unnamed_class = None
        last_ns = {}
        for cls_data in data:
            last_ns = cls_data.setdefault('Namespaces', last_ns.copy())
            if len(cls_data) == 1:
                continue
            cls_name = cls_data.get('Name')
            if not cls_name:
                if unnamed_class:
                    raise exceptions.AmbiguousClassName(name)
                unnamed_class = cls_data
            else:
                ns_resolver = namespace_resolver.NamespaceResolver(last_ns)
                cls_name = ns_resolver.resolve_name(cls_name)
                if cls_name == name:
                    type_obj = murano_type.create(
                        cls_data, self, cls_name, ns_resolver)
                    self._classes[name] = type_obj
                else:
                    self._load_queue.setdefault(cls_name, cls_data)
        if type_obj is None and unnamed_class:
            unnamed_class['Name'] = name
            return self._register_mpl_classes(unnamed_class, name)
        return type_obj

    def _register_native_class(self, cls, name):
        if cls in self._imported_types:
            return self._classes[name]

        try:
            m_class = self.find_class(name, False)
        except exceptions.NoClassFound:
            m_class = self._register_mpl_classes({'Name': name}, name)

        m_class.extension_class = cls

        for method_name in dir(cls):
            if method_name.startswith('_'):
                continue
            method = getattr(cls, method_name)
            if not any((
                    helpers.inspect_is_method(cls, method_name),
                    helpers.inspect_is_static(cls, method_name),
                    helpers.inspect_is_classmethod(cls, method_name))):
                continue
            method_name_alias = (getattr(
                method, '__murano_name', None) or
                specs.convert_function_name(
                    method_name, yaql_integration.CONVENTION))
            m_class.add_method(method_name_alias, method, method_name)
        self._imported_types.add(cls)
        return m_class

    def register_class(self, cls, name=None):
        if inspect.isclass(cls):
            name = name or getattr(cls, '__murano_name', None) or cls.__name__
            if name in self._classes:
                self._register_native_class(cls, name)
            else:
                self._native_load_queue.setdefault(name, cls)
        elif isinstance(cls, dsl_types.MuranoType):
            self._classes[cls.name] = cls
        elif name not in self._classes:
            self._load_queue[name] = cls

    def find_class(self, name, search_requirements=True):
        payload = self._native_load_queue.pop(name, None)
        if payload is not None:
            return self._register_native_class(payload, name)

        payload = self._load_queue.pop(name, None)
        if payload is not None:
            result = self._register_mpl_classes(payload, name)
            if result:
                return result

        result = self._classes.get(name)
        if result:
            return result
        if search_requirements:
            pkgs_for_search = []
            for package_name, version_spec in six.iteritems(
                    self._requirements):
                if package_name == self.name:
                    continue
                referenced_package = self._package_loader.load_package(
                    package_name, version_spec)
                try:
                    return referenced_package.find_class(name, False)
                except exceptions.NoClassFound:
                    if name.startswith('io.murano.extensions'):
                        try:
                            short_name = name.replace(
                                'io.murano.extensions.', '', 1)
                            result = referenced_package.find_class(
                                short_name, False)
                            warnings.simplefilter("once")
                            msg = ("Plugin %(name)s was not found, but a "
                                   "%(shorter_name)s was found instead and "
                                   "will be used. This could be caused by "
                                   "recent change in plugin naming scheme. If "
                                   "you are developing applications targeting "
                                   "this plugin consider changing its name" %
                                   {'name': name, 'shorter_name': short_name})
                            debtcollector.deprecate(msg)
                            return result
                        except exceptions.NoClassFound:
                            pass
                    pkgs_for_search.append(referenced_package)
                    continue
            raise exceptions.NoClassFound(
                name, packages=pkgs_for_search + [self])

        raise exceptions.NoClassFound(name, packages=[self])

    @property
    def context(self):
        return None

    def _create_package_class(self):
        ns_resolver = namespace_resolver.NamespaceResolver(None)
        return murano_type.MuranoClass(
            ns_resolver, self.name, self, utils.NO_VALUE)

    def get_meta(self, context):
        if six.callable(self._meta):
            executor = helpers.get_executor()
            context = executor.create_package_context(self)
            self._meta = self._meta().get_meta(context)
        return self._meta

    def __repr__(self):
        return 'MuranoPackage({name})'.format(name=self.name)
