# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://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 <https://www.gnu.org/licenses/>.
"""Instrumentation utilities"""

try:
    import pygraphviz
except ImportError:
    pygraphviz = None

from cubicweb.cwvreg import CWRegistryStore
from cubicweb.devtools.devctl import DevConfiguration


ALL_COLORS = [
    "00FF00",
    "0000FF",
    "FFFF00",
    "FF00FF",
    "00FFFF",
    "000000",
    "800000",
    "008000",
    "000080",
    "808000",
    "800080",
    "008080",
    "808080",
    "C00000",
    "00C000",
    "0000C0",
    "C0C000",
    "C000C0",
    "00C0C0",
    "C0C0C0",
    "400000",
    "004000",
    "000040",
    "404000",
    "400040",
    "004040",
    "404040",
    "200000",
    "002000",
    "000020",
    "202000",
    "200020",
    "002020",
    "202020",
    "600000",
    "006000",
    "000060",
    "606000",
    "600060",
    "006060",
    "606060",
    "A00000",
    "00A000",
    "0000A0",
    "A0A000",
    "A000A0",
    "00A0A0",
    "A0A0A0",
    "E00000",
    "00E000",
    "0000E0",
    "E0E000",
    "E000E0",
    "00E0E0",
    "E0E0E0",
]
_COLORS = {}


def get_color(key):
    try:
        return _COLORS[key]
    except KeyError:
        _COLORS[key] = "#" + ALL_COLORS[len(_COLORS) % len(ALL_COLORS)]
        return _COLORS[key]


def warn(msg, *args):
    print(f"WARNING: {msg % args}")


def info(msg):
    print("INFO: " + msg)


class PropagationAnalyzer:
    """Abstract propagation analyzer, providing utility function to extract
    entities involved in propagation from a schema, as well as propagation
    rules from hooks (provided they use intrumentalized sets, see
    :class:`CubeTracerSet`).

    Concrete classes should at least define `prop_rel` class attribute and
    implements the `is_root` method.

    See `localperms` or `nosylist` cubes for example usage (`ccplugin` module).
    """

    prop_rel = None  # name of the propagation relation

    def init(self, cube):
        """Initialize analyze for the given cube, returning the (already loaded)
        vregistry and a set of entities which we're interested in.
        """
        config = DevConfiguration(cube)
        schema = config.load_schema()
        vreg = CWRegistryStore(config)
        vreg.set_schema(schema)  # set_schema triggers objects registrations
        eschemas = {
            eschema for eschema in schema.entities() if self.should_include(eschema)
        }
        return vreg, eschemas

    def is_root(self, eschema):
        """Return `True` if given entity schema is a root of the graph"""
        raise NotImplementedError()

    def should_include(self, eschema):
        """Return `True` if given entity schema should be included by the graph."""

        if self.prop_rel in eschema.subject_relations or self.is_root(eschema):
            return True
        return False

    def prop_edges(self, s_rels, o_rels, eschemas):
        """Return a set of edges where propagation has been detected.

        Each edge is defined by a 4-uple (from node, to node, rtype, package)
        where `rtype` is the relation type bringing from <from node> to <to
        node> and `package` is the cube adding the rule to the propagation
        control set (see see :class:`CubeTracerSet`).
        """
        schema = iter(eschemas).next().schema
        prop_edges = set()
        for rtype in s_rels:
            found = False
            for subj, obj in schema.relation_schema_for(rtype).relation_definitions:
                if subj in eschemas and obj in eschemas:
                    found = True
                    prop_edges.add((subj, obj, rtype, s_rels.value_cube[rtype]))
            if not found:
                warn("no rdef match for %s", rtype)
        for rtype in o_rels:
            found = False
            for subj, obj in schema.relation_schema_for(rtype).relation_definitions:
                if subj in eschemas and obj in eschemas:
                    found = True
                    prop_edges.add((obj, subj, rtype, o_rels.value_cube[rtype]))
            if not found:
                warn("no rdef match for %s", rtype)
        return prop_edges

    def detect_problems(self, eschemas, edges):
        """Given the set of analyzed entity schemas and edges between them,
        return a set of entity schemas where a problem has been detected.
        """
        problematic = set()
        for eschema in eschemas:
            if self.has_problem(eschema, edges):
                problematic.add(eschema)
        not_problematic = set(eschemas).difference(problematic)
        if not_problematic:
            info(
                f"nothing problematic in: {', '.join(e.type for e in not_problematic)}"
            )
        return problematic

    def has_problem(self, eschema, edges):
        """Return `True` if the given schema is considered problematic,
        considering base propagation rules.
        """
        root = self.is_root(eschema)
        has_prop_rel = self.prop_rel in eschema.subject_relations
        # root but no propagation relation
        if root and not has_prop_rel:
            warn("%s is root but miss %s", eschema, self.prop_rel)
            return True
        # propagated but without propagation relation / not propagated but
        # with propagation relation
        if not has_prop_rel and any(edge for edge in edges if edge[1] == eschema):
            warn("%s miss %s but is reached by propagation", eschema, self.prop_rel)
            return True
        elif has_prop_rel and not root:
            rdef = eschema.rdef(self.prop_rel, take_first=True)
            edges = [edge for edge in edges if edge[1] == eschema]
            if not edges:
                warn(
                    "%s has %s but isn't reached by " "propagation",
                    eschema,
                    self.prop_rel,
                )
                return True
            # require_permission relation / propagation rule not added by
            # the same cube
            elif not any(edge for edge in edges if edge[-1] == rdef.package):
                warn(
                    "%s has %s relation / propagation rule"
                    " not added by the same cube (%s / %s)",
                    eschema,
                    self.prop_rel,
                    rdef.package,
                    edges[0][-1],
                )
                return True
        return False

    def init_graph(self, eschemas, edges, problematic):
        """Initialize and return graph, adding given nodes (entity schemas) and
        edges between them.

        Require pygraphviz installed.
        """
        if pygraphviz is None:
            raise RuntimeError("pygraphviz is not installed")
        graph = pygraphviz.AGraph(strict=False, directed=True)
        for eschema in eschemas:
            if eschema in problematic:
                params = {"color": "#ff0000", "fontcolor": "#ff0000"}
            else:
                params = {}  # 'color': get_color(eschema.package)}
            graph.add_node(eschema.type, **params)
        for subj, obj, rtype, package in edges:
            graph.add_edge(str(subj), str(obj), label=rtype, color=get_color(package))
        return graph

    def add_colors_legend(self, graph):
        """Add a legend of used colors to the graph."""
        for package, color in sorted(_COLORS.items()):
            graph.add_node(package, color=color, fontcolor=color, shape="record")


class CubeTracerSet:
    """Dumb set implementation whose purpose is to keep track of which cube is
    being loaded when something is added to the set.

    Results will be found in the `value_cube` attribute dictionary.

    See `localperms` or `nosylist` cubes for example usage (`hooks` module).
    """

    def __init__(self, vreg, wrapped):
        self.vreg = vreg
        self.wrapped = wrapped
        self.value_cube = {}

    def add(self, value):
        self.wrapped.add(value)
        cube = self.vreg.currently_loading_cube
        if value in self.value_cube:
            warn(
                "%s is propagated by cube %s and cube %s",
                value,
                self.value_cube[value],
                cube,
            )
        else:
            self.value_cube[value] = cube

    def __iter__(self):
        return iter(self.wrapped)

    def __ior__(self, other):
        for value in other:
            self.add(value)
        return self

    def __ror__(self, other):
        other |= self.wrapped
        return other
