#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Format-agnostic representation of the output graph."""

import colorsys
import logging
import re


class Colorizer:
    """Output graph color manager.

    We set node color by filename.

    HSL: hue = top-level namespace, lightness = nesting level, saturation constant.

    The "" namespace (for *.py files) gets the first color. Since its
    level is 0, its lightness will be 1.0, i.e. pure white regardless
    of the hue.
    """

    def __init__(self, num_colors, colored=True, logger=None):
        self.logger = logger or logging.getLogger(__name__)
        self.colored = colored

        self._hues = [j / num_colors for j in range(num_colors)]
        self._idx_of = {}  # top-level namespace: hue index
        self._idx = 0

    def _next_idx(self):
        result = self._idx
        self._idx += 1
        if self._idx >= len(self._hues):
            self.logger.warn("WARNING: colors wrapped")
            self._idx = 0
        return result

    def _node_to_idx(self, node):
        ns = node.filename
        self.logger.info(f"Coloring {node.get_short_name()} from file '{ns}'")
        if ns not in self._idx_of:
            self._idx_of[ns] = self._next_idx()
        return self._idx_of[ns]

    def get(self, node):  # return (group number, hue index)
        idx = self._node_to_idx(node)
        return (idx, self._hues[idx])

    def make_colors(self, node):  # return (group number, fill color, text color)
        if self.colored:
            idx, H = self.get(node)
            L = max([1.0 - 0.1 * node.get_level(), 0.1])
            S = 1.0
            A = 0.7  # make nodes translucent (to handle possible overlaps)
            fill_RGBA = self.htmlize_rgb(*colorsys.hls_to_rgb(H, L, S), A=A)

            # black text on light nodes, white text on (very) dark nodes.
            text_RGB = "#000000" if L >= 0.5 else "#ffffff"
        else:
            idx, _ = self.get(node)
            fill_RGBA = self.htmlize_rgb(1.0, 1.0, 1.0, 0.7)
            text_RGB = "#000000"
        return idx, fill_RGBA, text_RGB

    @staticmethod
    def htmlize_rgb(R, G, B, A=None):
        if A is not None:
            R, G, B, A = [int(255.0 * x) for x in (R, G, B, A)]
            return f"#{R:02x}{G:02x}{B:02x}{A:02x}"
        else:
            R, G, B = [int(255.0 * x) for x in (R, G, B)]
            return f"#{R:02x}{G:02x}{B:02x}"


class VisualNode:
    """
    A node in the output graph: colors, internal ID, human-readable label, ...
    """

    def __init__(self, id, label="", flavor="", fill_color="", text_color="", group=""):
        self.id = id  # graphing software friendly label (no special chars)
        self.label = label  # human-friendly label
        self.flavor = flavor
        self.fill_color = fill_color
        self.text_color = text_color
        self.group = group

    def __repr__(self):
        optionals = [repr(s) for s in [self.label, self.flavor, self.fill_color, self.text_color, self.group] if s]
        if optionals:
            return "VisualNode(" + repr(self.id) + ", " + ", ".join(optionals) + ")"
        else:
            return "VisualNode(" + repr(self.id) + ")"


class VisualEdge:
    """
    An edge in the output graph.

    flavor is meant to be 'uses' or 'defines'
    """

    def __init__(self, source, target, flavor, color):
        self.source = source
        self.target = target
        self.flavor = flavor
        self.color = color

    def __repr__(self):
        return "Edge(" + self.source.label + " " + self.flavor + " " + self.target.label + ")"


class VisualGraph:
    def __init__(self, id, label, nodes=None, edges=None, subgraphs=None, grouped=False):
        self.id = id
        self.label = label
        self.nodes = nodes or []
        self.edges = edges or []
        self.subgraphs = subgraphs or []
        self.grouped = grouped

    @classmethod
    def from_visitor(cls, visitor, options=None, logger=None):
        colored = options.get("colored", False)
        nested = options.get("nested_groups", False)
        grouped_alt = options.get("grouped_alt", False)
        grouped = nested or options.get("grouped", False)  # nested -> grouped
        annotated = options.get("annotated", False)
        draw_defines = options.get("draw_defines", False)
        draw_uses = options.get("draw_uses", False)

        # Terminology:
        #  - what Node calls "label" is a computer-friendly unique identifier
        #    for use in graphing tools
        #  - the "label" property of a GraphViz node is a **human-readable** name
        #
        # The annotation determines the human-readable name.
        #
        if annotated:
            if grouped:
                # group label includes namespace already
                def labeler(n):
                    return n.get_annotated_name()

            else:
                # the node label is the only place to put the namespace info
                def labeler(n):
                    return n.get_long_annotated_name()

        else:

            def labeler(n):
                return n.get_short_name()

        logger = logger or logging.getLogger(__name__)

        # collect and sort defined nodes
        visited_nodes = []
        for name in visitor.nodes:
            for node in visitor.nodes[name]:
                if node.defined:
                    visited_nodes.append(node)
        visited_nodes.sort(key=lambda x: (x.namespace, x.name))

        def find_filenames():
            filenames = set()
            for node in visited_nodes:
                filenames.add(node.filename)
            return filenames

        colorizer = Colorizer(num_colors=len(find_filenames()) + 1, colored=colored, logger=logger)

        nodes_dict = dict()
        root_graph = cls("G", label="", grouped=grouped)
        subgraph = root_graph
        namespace_stack = []
        prev_namespace = ""  # The namespace '' is first in visited_nodes.
        for node in visited_nodes:
            logger.info(f"Looking at {node.name}")

            # Create the node itself and add it to nodes_dict
            idx, fill_RGBA, text_RGB = colorizer.make_colors(node)
            visual_node = VisualNode(
                id=node.get_label(),
                label=labeler(node),
                flavor=repr(node.flavor),
                fill_color=fill_RGBA,
                text_color=text_RGB,
                group=idx,
            )
            nodes_dict[node] = visual_node

            # next namespace?
            if grouped and node.namespace != prev_namespace:
                if not prev_namespace:
                    logger.info(f"New namespace {node.namespace}")
                else:
                    logger.info(f"New namespace {node.namespace}, old was {prev_namespace}")
                prev_namespace = node.namespace

                label = node.get_namespace_label()
                subgraph = cls(label, node.namespace)

                if nested:
                    # Pop the stack until the newly found namespace is within
                    # one of the parent namespaces, or until the stack runs out
                    # (i.e. this is a sibling).
                    if len(namespace_stack):
                        m = re.match(namespace_stack[-1].label, node.namespace)
                        # The '.' check catches siblings in cases like
                        # MeshGenerator vs. Mesh.
                        while m is None or m.end() == len(node.namespace) or node.namespace[m.end()] != ".":
                            namespace_stack.pop()
                            if not len(namespace_stack):
                                break
                            m = re.match(namespace_stack[-1].label, node.namespace)
                    parentgraph = namespace_stack[-1] if len(namespace_stack) else root_graph
                    parentgraph.subgraphs.append(subgraph)

                    namespace_stack.append(subgraph)
                else:
                    root_graph.subgraphs.append(subgraph)

            subgraph.nodes.append(visual_node)

        # Now add edges
        if draw_defines or grouped_alt:
            # If grouped, use gray lines so they won't visually obstruct
            # the "uses" lines.
            #
            # If not grouped, create lines for defines, but make them
            # fully transparent. This helps GraphViz's layout algorithms
            # place closer together those nodes that are linked by a
            # defines relationship.
            #
            color = "#838b8b" if draw_defines else "#ffffff00"
            for n in visitor.defines_edges:
                if n.defined:
                    for n2 in visitor.defines_edges[n]:
                        if n2.defined:
                            root_graph.edges.append(VisualEdge(nodes_dict[n], nodes_dict[n2], "defines", color))

        if draw_uses:
            color = "#000000"
            for n in visitor.uses_edges:
                if n.defined:
                    for n2 in visitor.uses_edges[n]:
                        if n2.defined:
                            root_graph.edges.append(VisualEdge(nodes_dict[n], nodes_dict[n2], "uses", color))

        return root_graph
