# copyright 2015-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr -- mailto:contact@logilab.fr
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""cubicweb-compound application package

Library cube to handle assemblies of composite entities
"""

from functools import wraps

from cubicweb import neg_role
from cubicweb.schema import META_RTYPES, WORKFLOW_RTYPES, SYSTEM_RTYPES


def skip_rtypes_set(skiprtypes=()):
    """Return rtypes to be skipped, including special rtypes from cubicweb."""
    return frozenset(set(skiprtypes) | META_RTYPES | WORKFLOW_RTYPES | SYSTEM_RTYPES)


def skip_meta(func):
    """Decorator to set `skiprtypes` and `skipetypes` parameters as frozensets
    including "meta" objects that should be implicitely ignored.
    """
    @wraps(func)
    def wrapped(*args, **kwargs):
        kwargs['skiprtypes'] = skip_rtypes_set(kwargs.get('skiprtypes', ()))
        kwargs['skipetypes'] = frozenset(kwargs.get('skipetypes', ()))
        return func(*args, **kwargs)
    return wrapped


@skip_meta
def structure_def(schema, etype, skiprtypes=(), skipetypes=()):
    """Return the container structure with `etype` as root entity.

    This structure is a dictionary with entity types as keys and `(relation
    type, role)` as values. These entity types are reachable through composite
    relations from the root <etype>. Each key gives the name of an entity
    type, associated to a list of relation/role allowing to access to its
    parent (which may be the container or a contained).
    """
    graph = CompositeGraph(schema, skiprtypes=skiprtypes, skipetypes=skipetypes)
    return dict((child, set(relinfo))
                for child, relinfo in graph.parent_structure(etype).items())


class CompositeGraph(object):
    """Represent a graph of entity types related through composite relations.

    A `CompositeGraph` can be used to iterate on schema objects through
    `parent_relations`/`child_relations` methods as well as on entities through
    `parent_related`/`child_related` methods.
    """

    @skip_meta
    def __init__(self, schema, skiprtypes=(), skipetypes=()):
        self.schema = schema
        self.skiprtypes = skiprtypes
        self.skipetypes = skipetypes

    def parent_relations(self, etype):
        """Yield graph relation information items walking the graph upstream
        from `etype`.

        These items are `(rtype, role), parents` where `parents` is a list of
        possible parent entity types reachable through `(rtype, role)`
        relation.
        """
        return self._graph_relations(etype, topdown=False)

    def child_relations(self, etype):
        """Yield graph relation information items walking the graph downstream
        from `etype`.

        These items are `(rtype, role), children` where `children` is a list of
        possible child entity types reachable through `(rtype, role)` relation.
        """
        return self._graph_relations(etype, topdown=True)

    def _graph_relations(self, etype, topdown, follow_relations=()):
        """Yield `(rtype, role), etypes` values corresponding to arcs of the
        graph of entity types reachable from a `etype` by following composite
        relations and relations selected by `follow_relations` parameter at
        initialization. `etypes` is a list of possible entity types reachable
        through `(rtype, role)` relation "upstream" (resp. "downstream") if
        `topdown` is True (resp. False).
        """
        try:
            eschema = self.schema[etype]
        except KeyError:
            return
        for rschema, teschemas, role in eschema.relation_definitions():
            if rschema.meta or rschema in self.skiprtypes:
                continue
            target_role = role if topdown else neg_role(role)
            relation, children = (rschema.type, role), []
            for target in teschemas:
                if target in self.skipetypes:
                    continue
                rdef = rschema.role_rdef(eschema, target, role)
                if ((rschema.type, target_role) not in follow_relations
                        and rdef.composite != target_role):
                    continue
                children.append(target.type)
            if children:
                yield relation, children

    def parent_structure(self, etype, _visited=None):
        """Return the parent structure of the graph from `etype`.

        The structure is a dictionary mapping entity type in the graph with
        root `etype` to relation information allowing to walk the graph
        upstream from this entity type.
        """
        if _visited is None:
            _visited = set()
        structure = {}

        def update_structure(left, relation, right):
            structure.setdefault(left, {}).setdefault(relation, []).append(right)

        for (rtype, role), children in self.child_relations(etype):
            for child in sorted(children):
                update_structure(child, (rtype, neg_role(role)), etype)
                if child in _visited:
                    continue
                _visited.add(child)
                for left, rels in self.parent_structure(child, _visited).items():
                    for relation, rights in rels.items():
                        for right in rights:
                            update_structure(left, relation, right)
        return structure

    def parent_related(self, entity):
        """Yield information items on entities related to `entity` through
        composite relations walking the graph upstream from `entity`.

        These items are tuples `(child, (rtype, role), parent)` where `role` is
        the role of `child` entity in `rtype` relation with `parent`.
        """
        return self._graph_related(entity, False)

    def child_related(self, entity, follow_relations=()):
        """Yield information items on entities related to `entity` through
        composite relations walking the graph downstream from `entity`.

        These items are tuples `(parent, (rtype, role), child)` where `role` is
        the role of `parent` entity in `rtype` relation with `child`.
        """
        return self._graph_related(entity, True,
                                   follow_relations=follow_relations)

    def _graph_related(self, entity, topdown, follow_relations=(),
                       _visited=None):
        """Yield arcs of the graph of made from entities reachable from
        `entity` through composite relations or relations specified in
        `follow_relations`.

        An "arc" is a tuple `(l_entity, (rtype, role), r_entity)` where
        `l_entity` is a "parent" (resp. "child") entity when `topdown` is True
        (resp. False) and, conversely, `r_entity` is the "child" (resp.
        "parent") entity. `role` is always the role of `l_entity` in `rtype`
        relation.
        """
        if _visited is None:
            _visited = set()
        for (rtype, role), targettypes in self._graph_relations(
                entity.cw_etype, topdown, follow_relations=follow_relations):
            rset = entity.related(rtype, role=role, targettypes=targettypes)
            for target in rset.entities():
                yield entity, (rtype, role), target
                if target.eid in _visited:
                    continue
                _visited.add(target.eid)
                for x in self._graph_related(
                        target, topdown, follow_relations=follow_relations,
                        _visited=_visited):
                    yield x
