"""
This module provides a means to automatically layout diagrams.

The layout is done like this:
 - First all nodes (Classes, packages, comments) on a diagram are determined
 - A vertical ordering is determined based on the inheritance
 - A horizontal ordering is determined based on associations and dependencies
 - The nodes are moved to their place
 - Lines are reconnected to the nodes, so everything looks pretty.

"""

import logging
import random

from gaphor.core import action, build_action_group, transactional
from gaphor.diagram.presentation import LinePresentation
from gaphor.diagram.classes import GeneralizationItem, ImplementationItem
from gaphor.abc import Service, ActionProvider
from gaphor.plugins.diagramlayout import toposort
from gaphor.ui.abc import UIComponent

log = logging.getLogger(__name__)


class DiagramLayout(Service, ActionProvider):

    menu_xml = """
      <ui>
        <menubar action="mainwindow">
          <menu action="diagram">
            <menuitem action="diagram-layout" />
          </menu>
        </menubar>
      </ui>"""

    def __init__(self, component_registry):
        self.component_registry = component_registry
        self.action_group = build_action_group(self)

    def shutdown(self):
        pass

    def get_current_diagram(self):
        return self.component_registry.get(
            UIComponent, "diagrams"
        ).get_current_diagram()

    def update(self):
        self.sensitive = bool(self.get_current_diagram())

    @action(
        name="diagram-layout", label="La_yout diagram", tooltip="simple diagram layout"
    )
    def execute(self):
        d = self.get_current_diagram()
        self.layout_diagram(d)

    @transactional
    def layout_diagram(self, diag):
        layout_diagram(diag)


MARGIN = 100


def layout_diagram(diag):
    """
    So an attempt to layout (order) the items on a diagram. The items
    should already be placed on the diagram and the items should already be
    connected.

    This function works on the diagram items (hence it does not check relations
    in the datamodel, only the ones drawn on the diagram) to produce a
    decent layout.
    """
    nodes = []
    primary_nodes = []
    relations = []
    other_relations = []

    # Make sure all items are updated
    diag.canvas.update_now()

    # First extract data from the diagram (which ones are the nodes, and
    # the relationships).
    for item in diag.canvas.get_root_items():
        if isinstance(item, (GeneralizationItem, ImplementationItem)):
            # Primary relationships, should be drawn top-down
            try:
                relations.append(
                    (item.handles[0].connected_to, item.handles[-1].connected_to)
                )
                primary_nodes.extend(relations[-1])
            except Exception as e:
                log.error(e)
        elif isinstance(item, LinePresentation):
            # Secondary (associations, dependencies) may be drawn top-down
            # or left-right
            try:
                other_relations.append(
                    (item.handles[0].connected_to, item.handles[-1].connected_to)
                )
                # other_relations.append((item.handles[-1].connected_to,
                #                        item.handles[0].connected_to))
            except Exception as e:
                log.error(e)
        else:
            nodes.append(item)

    # Add some randomness:
    random.shuffle(other_relations)
    primary_nodes = uniq(primary_nodes)

    # Find out our horizontal and vertical sorting
    sorted = toposort.toposort(nodes, relations, 0)
    other_sorted = toposort.toposort(nodes, other_relations, 0)

    if not sorted:
        return

    # Move nodes from the first (generalization) row to the other rows
    # if they are not superclasses for some other class
    # Run the list twice, just to ensure no items are left behind.
    for item in list(sorted[0]) * 2:
        if item not in primary_nodes and item in sorted[0]:
            # Find nodes that do have a relation to this one
            related = find_related_nodes(item, other_relations)
            # Figure out what row(s) they're on
            row = find_row(item, related, sorted[1:])
            if row:
                # print 'moving', item.subject.name, 'to row', sorted.index(row)
                sorted[0].remove(item)
                row.append(item)

    # Order each row based on the sort order of other_sorted
    # (the secondary sort alg.).
    for row in sorted:
        for other_row in other_sorted:
            for other_item in other_row:
                if other_item in row:
                    row.remove(other_item)
                    row.append(other_item)

    # Place the nodes on the diagram.
    y = MARGIN / 2
    for row in sorted:
        x = MARGIN / 2
        maxy = 0
        for item in row:
            if not item:
                continue
            maxy = max(maxy, item.height)
            a = item.matrix
            a = (a[0], a[1], a[2], a[3], x, y)
            item.matrix = a
            item.request_update()
            x += item.width + MARGIN
        y += maxy + MARGIN

    # Reattach the relationships to the nodes, in a way that it looks nice.
    simple_layout_lines(diag)


def simple_layout_lines(diag):
    """
    Just do the layout of the lines in a diagram. The nodes (class, package)
    are left where they are (use layout_diagram() if you want to reorder
    everything).

    The line layout is basically very simple: just draw straight lines
    between nodes on the diagram.
    """
    lines = {}
    for item in diag.canvas.get_root_items():
        if isinstance(item, LinePresentation):
            # Secondary (associations, dependencies) may be drawn top-down
            # or left-right
            try:
                lines[item] = (
                    item.handles[0].connected_to,
                    item.handles[-1].connected_to,
                )
            except Exception as e:
                log.error(e)

    # Now we have the lines, let's first ensure we only have a begin and an
    # end handle
    for line in list(lines.keys()):
        while len(line.handles) > 2:
            line.set_property("del_segment", 0)

    # Strategy 1:
    # Now we have nice short lines. Let's move them to a point between
    # both nodes and let the connect() do the work:
    for line, nodes in list(lines.items()):
        if not nodes[0] or not nodes[1]:
            # loose end
            continue
        center0 = find_center(nodes[0])
        center1 = find_center(nodes[1])
        center = ((center0[0] + center1[0]) / 2.0, (center0[1] + center1[1]) / 2.0)
        line.handles[0].set_pos_w(*center)
        line.handles[-1].set_pos_w(*center)
        nodes[0].connect_handle(line.handles[0])
        nodes[1].connect_handle(line.handles[-1])


def uniq(lst):
    d = {}
    for l in lst:
        d[l] = None
    return list(d.keys())


def find_related_nodes(item, relations):
    """
    Find related nodes of item, given a list of tuples.
    References to itself are ignored.
    """
    related = []
    for pair in relations:
        if pair[0] is item:
            if pair[1] is not item:
                related.append(pair[1])
        elif pair[1] is item:
            if pair[0] is not item:
                related.append(pair[0])
    return uniq(related)


def find_row(item, related_items, sorted):
    """
    Find the row that contains the most references to item.
    """
    max_refs = 0
    max_row = None
    for row in sorted:
        cnt = len([i for i in row if i in related_items])
        if cnt > max_refs:
            max_row = row
            max_refs = cnt
    return max_row


def find_center(item):
    """
    Find the center point of the item, in world coordinates
    """
    x = item.width / 2.0
    y = item.height / 2.0
    return item.canvas.get_matrix_i2c(item).transform_point(x, y)


# vim:sw=4:et
