"""
TrivalentGraphModel.py

$Id$

bug: rubber edges stopped working...
"""

from demoapp.geometry.vectors import V, dot, vlen, unitVector, rotate2d_90

import pyglet

from demoapp.graphics.colors import black

from demoapp.graphics.drawing import drawline2d

from demoapp.models.TrivalentGraph_Graphics import NODE_RADIUS, draw_Node

# ==

class ModelComponent(object): #refile
    def hit_test(self, x, y):
        return False

class Node(ModelComponent):
    radius = NODE_RADIUS
    def __init__(self, model, x, y):
        self.model = model
        self.x = x
        self.y = y
        self.edges = [] # sequence or set of our edges
    def destroy(self):
        for e in self.edges[:]:
            e.destroy()
        self.edges = []
        del self.model.nodes[self]
    def new_edge_ok(self):
        return len(self.edges) < 3
    def new_edge_ok_to(self, node):
        return self.new_edge_ok() and not self.connected_to(node)
    def connected_to(self, node):
        for edge in self.edges:
            if node in edge.nodes:
                return True
        return False
    def draw(self, highlight_color = None):
        draw_Node( self.pos,
                   self.radius,
                   numedges = len(self.edges),
                   highlight_color = highlight_color)
    def get_pos(self):
        return V(self.x, self.y)
    def set_pos(self, pos):
        self.x, self.y = pos
    pos = property(get_pos, set_pos)
    def hit_test(self, x, y): # modified from soundspace.py; made 2d; could improve using vectors.py functions
        dx, dy  = [a - b for a, b in zip(self.pos, (x, y))]
        if dx * dx + dy * dy  < self.radius * self.radius:
            return -dx, -dy,  # return relative position within object
    pass

class Edge(ModelComponent):
    def __init__(self, model, n1, n2):
        assert isinstance(n1, Node)
        assert isinstance(n2, Node)
        assert n1 is not n2
        assert not n1.connected_to(n2)
        assert not n2.connected_to(n1)
        self.model = model
        self.nodes = (n1, n2) # sequence or set of our nodes
        for n in self.nodes:
            n.edges.append(self)
    def destroy(self): # (what about undo?)
        del self.model.edges[self]
        for n in self.nodes:
            n.edges.remove(self)
        self.nodes = ()
    def draw(self):
        drawline2d(black, self.n1.pos, self.n2.pos) # refactor, use in rubber_edge: draw_Edge
    @property
    def n1(self):
        return self.nodes[0]
    @property
    def n2(self):
        return self.nodes[1]
    def other(self, node):
        if self.nodes[0] is node:
            return self.nodes[1]
        else:
            assert self.nodes[1] is node
            return self.nodes[0]
        pass
    def hit_test(self, x, y):
        HALO_RADIUS = 2 # or maybe 1?
        p1 = self.nodes[0].pos
        p2 = self.nodes[1].pos
        direction_along_edge = unitVector(p2 - p1)
        unit_normal_to_edge = rotate2d_90(direction_along_edge)
        vec = V(x,y) - p1
        distance_from_edge_line = abs(dot(vec, unit_normal_to_edge))
        if distance_from_edge_line > HALO_RADIUS:
            return False
        distance_along_edge_line = dot(vec, direction_along_edge)
        if HALO_RADIUS <= distance_along_edge_line <= vlen(p2 - p1) - HALO_RADIUS:
            # For our purposes, exclude points too near the endpoints --
            # matches to a rectangle centered on the edge and not within
            # halo radius (along the edge) of the endpoints. (If node radius
            # is larger, near-endpoint behavior won't matter anyway, since we
            # treat nodes as being in front of edges.)
            return True
        return False
    pass
    
class TrivalentGraphModel(object):
    # REVISE: rename, then split out superclass Model
    """
    model for a partial or complete trivalent graph
    """
    Node = Node
    Edge = Edge
    
    def __init__(self):
        self.nodes = {}
        self.edges = {}
        
    def cmd_addNode(self, x, y):
        n = self.Node(self, x, y)
        self.nodes[n] = n
        #print "added", n
        return n

    def cmd_deleteNode(self, n):
        for e in list(n.edges):
            e.destroy()
        del self.nodes[n]
        return True # only needed due to an assert that is wrong in general
    
    def cmd_addEdge(self, n1, n2):
        e = self.Edge(self, n1, n2)
        self.edges[e] = e
        #print "added", e
        return e

    def cmd_addNodeAndConnectFrom(self, x, y, node):
        # 'From' in cmdname is because node order within edge
        # might matter someday, for a directed graph
        node2 = self.cmd_addNode(x, y)
        self.cmd_addEdge(node, node2)
        return node2

    def cmd_addNodeOnEdge(self, x, y, edge):
        node2 = self.cmd_addNode(x, y) # todo: position it exactly on edge? if so, worry if mouse still over it re KLUGE elsewhere
        n1, n3 = list(edge.nodes) # seq or set; in order, if that matters
        edge.destroy()
        self.cmd_addEdge(n1, node2)
        self.cmd_addEdge(node2, n3)
        return node2

    def cmd_addNodeOnEdgeAndConnectFrom(self, x, y, edge, node):
        node2 = self.cmd_addNodeOnEdge(x, y, edge)
        self.cmd_addEdge(node, node2)
        return node2

    def cmd_MergeNodes(self, node1, node2):
        "merge node1 with node2 (retaining properties of node2, e.g. .pos)"
        node1_neighbors = [e.other(node1) for e in node1.edges] # might include node2
        node1.destroy()
        for n in node1_neighbors:
            if n is not node2 and not n.connected_to(node2):
                self.cmd_addEdge(n, node2)
        return True
    
    def hit_test_objects(self): # front to back
        for node in self.nodes.itervalues():
            yield node
        for edge in self.edges.itervalues():
            yield edge

    def drawn_objects(self):
        return self.hit_test_objects() #stub

    def draw(self):
        for obj in self.drawn_objects():
            obj.draw()

    pass