#    Copyright (c) 2016 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 abc
import operator
import weakref

import six

from murano.dsl import dsl_types
from murano.dsl import helpers


class MetaProvider(object):
    @abc.abstractmethod
    def get_meta(self, context):
        raise NotImplementedError()


class MetaData(MetaProvider):
    def __init__(self, definition, target, declaring_type):
        declaring_type = weakref.proxy(declaring_type)
        definition = helpers.list_value(definition)
        factories = []
        used_types = set()
        for d in definition:
            if isinstance(d, dict):
                if len(d) != 1:
                    raise ValueError('Invalid Meta format')
                name = next(iter(d.keys()))
                props = d[name] or {}
            else:
                name = d
                props = {}
            type_obj = helpers.resolve_type(name, declaring_type)
            if type_obj.usage != dsl_types.ClassUsages.Meta:
                raise ValueError('Only Meta classes can be attached')
            if target not in type_obj.targets:
                raise ValueError(
                    u'Meta class {} is not applicable here'.format(
                        type_obj.name))
            if type_obj in used_types and (
                    type_obj.cardinality != dsl_types.MetaCardinality.Many):
                raise ValueError('Cannot attach several Meta instances '
                                 'with cardinality One')

            used_types.add(type_obj)

            def factory_maker(template):
                def instantiate(context):
                    obj = helpers.get_object_store().load(
                        template, owner=None,
                        context=context, scope_type=declaring_type)
                    obj.declaring_type = declaring_type
                    return obj
                return instantiate

            factories.append(factory_maker({type_obj: props}))
        self._meta_factories = factories
        self._meta = None

    def get_meta(self, context):
        if self._meta is None:
            self._meta = list(map(lambda x: x(context), self._meta_factories))
        return self._meta


def merge_providers(initial_class, producer, context):
    def merger(cls_list, skip_list):
        result = set()
        all_meta = []
        for cls in cls_list:
            cls_skip_list = skip_list.copy()
            provider = producer(cls)
            meta = [] if provider is None else provider.get_meta(context)
            for item in meta:
                cardinality = item.type.cardinality
                inherited = item.type.inherited
                if cls != initial_class and (
                        not inherited or item.type in skip_list):
                    continue
                if cardinality == dsl_types.MetaCardinality.One:
                    cls_skip_list.add(item.type)
                all_meta.append((cls, item))
            all_meta.extend(merger(cls.parents, cls_skip_list))
        meta_types = {}
        for cls, item in all_meta:
            entry = meta_types.get(item.type)
            if entry is not None:
                if entry != cls:
                    raise ValueError(
                        u'Found more than one instance of Meta {} '
                        u'with Cardinality One'.format(item.type.name))
                else:
                    continue

            if item.type.cardinality == dsl_types.MetaCardinality.One:
                meta_types[item.type] = cls
            result.add((cls, item))
        return result

    meta = merger([initial_class], set())
    return list(six.moves.map(operator.itemgetter(1), meta))


def aggregate_meta(provider, context, group_by_name=True):
    key_func = lambda m: m.type.name if group_by_name else m.type
    meta = provider.get_meta(context)
    result = {}
    for item in meta:
        if item.type.cardinality == dsl_types.MetaCardinality.One:
            result[key_func(item)] = item
        else:
            result.setdefault(key_func(item), []).append(item)
    return result
