# Copyright 2008 Nanorex, Inc.  See LICENSE file for details. 
"""
ExternalBondSet.py - keep track of external bonds, to optimize redraw

@author: Bruce
@version: $Id$
@copyright: 2008 Nanorex, Inc.  See LICENSE file for details.

"""

# plan:
#
# initial test implem: do all bond drawing through this object,
# but do it just as often as right now, in the same way.
#
# see chunk._draw_external_bonds

# for now, we just maintain them but do nothing else with them, as of 080702 noon PT


from geometry.VQT import V

from graphics.drawing.ColorSorter import ColorSorter
from graphics.drawing.ColorSorter import ColorSortedDisplayList # not yet used?


_DEBUG_EBSET = False # DO NOT COMMIT WITH TRUE


class ExternalBondSet(object):
    """
    Know about all external bonds between two specific chunks,
    and cache info to speed their redrawing, including display lists.
    When the appearance or set of bonds change, we might be invalidated,
    or destroyed and remade, depending on client code.
    """
    def __init__(self, chunk1, chunk2):
        self.chunks = (chunk1, chunk2) # note: not private
        self._bonds = {}
        self._invalid = True # since we have no display list for drawing
        return

    def other_chunk(self, chunk):
        """
        @see: Bond.other_chunk
        """
        c1, c2 = self.chunks
        if chunk is c1:
            return c2
        elif chunk is c2:
            return c1
        assert 0
        return
    
    def add_bond(self, bond):
        """
        Add this bond to self, if not already there.
        It must be a bond between our two chunks (not checked).
        Do appropriate invals within self.
        """
        # removed for speed: assert self._correct_bond(bond)
        if not self._bonds.has_key(id(bond)):
            # test is to avoid needless invalidation (important optim)
            self._invalid = True
            self._bonds[id(bond)] = bond
            if _DEBUG_EBSET:
                print "added bond %r to %r" % (bond, self)
        return

    def empty(self):
        return not self._bonds

    def remove_incorrect_bonds(self):
        """
        Some of our bonds may have been killed, or their atoms have changed,
        or their atom parents (chunks) have changed, so they are no longer
        correctly our members. Remove any such bonds, and do appropriate
        invals within self.
        """
        bad = []
        for bond in self._bonds.itervalues():
            if not self._correct_bond(bond):
                bad.append(bond)
        if bad:
            self._invalid = True # REVIEW: do more, in a method?
            for bond in bad:
                del self._bonds[id(bond)]
                if _DEBUG_EBSET:
                    print "removed bond %r from %r" % (bond, self)
        return

    def _correct_bond(self, bond):
        """
        Is bond, which once belonged in self, still ok to have in self?
        If not, it's because it was killed, or its atoms are not in both
        of self's chunks.
        """
        # bond must not be killed
        if bond.killed():
            return False
        if bond not in bond.atom1.bonds:
            # This ought to be checked by bond.killed, but isn't yet!
            # See comment therein. REVIEW: Hopefully it works now and bond.killed
            # can be fixed to check it. Conversely, if it doesn't work right,
            # this routine will probably have bugs of its own.
            # (Note: it'd be nice if that was faster, but there might be old code that
            # relies on looking at atoms of a killed bond, so we can't just
            # set the atoms to None. OTOH we don't want to waste Undo time & space
            # with a "killed flag" (for now).)
            return False
        # bond's atoms must (still) point to both of our chunks
        c1 = bond.atom1.molecule
        c2 = bond.atom2.molecule
        return (c1, c2) == self.chunks or (c2, c1) == self.chunks # REVIEW: too slow due to == ?

    def destroy(self):
        if not self.chunks:
            return # permit repeated destroy
        if _DEBUG_EBSET:
            print "destroying %r" % self
        for chunk in self.chunks:
            chunk._f_remove_ExternalBondSet(self)
        self.chunks = ()
        self._bonds = () # make len() still work
        ### TODO: other deallocations, e.g. of display lists
        return

    def __repr__(self):
        res = "<%s at %#x with %d bonds for %r>" % \
              (self.__class__.__name__.split('.')[-1],
               id(self),
               len(self._bonds),
               self.chunks # this is () if self is destroyed
              )
        return res

    # ==

    # methods needed only for drawing
    
    def bounding_lozenge(self):
        center, radius = self.bounding_sphere()
        return center, center, radius

    def bounding_sphere(self): # note: abs coords, even after we have display list and permit relative motion
        ### STUB
        # in future we'll compute a real one (though it may be a loose approximation),
        # then cache it when we redraw display list, then transform here into abs coords
        center = V(0,0,0)
        radius = 10.0**9
        return center, radius

    def should_draw_as_picked(self):
        # stub: ask one of our bonds
        # (kluge: doesn't matter which one, answer depends only on their chunks)
        # todo: clean this up by defining this directly, duplicating code from
        # Bond.should_draw_as_picked
        for bond in self._bonds.itervalues():
            return bond.should_draw_as_picked()
    
    def draw(self, glpane, disp, color, drawLevel): # selected? highlighted?
        # initial testing stub -- just draw in immediate mode, in the same way
        # as if we were not being used.
        # (notes for a future implem: displist still valid (self._invalid)? culled?)

        # modified from Chunk._draw_external_bonds:
        
        use_outer_colorsorter = True # not sure whether/why this is needed
        
        if use_outer_colorsorter:
            ColorSorter.start(None)

        for bond in self._bonds.itervalues():
            bond.draw(glpane, disp, color, drawLevel)
        
        if use_outer_colorsorter:
            ColorSorter.finish()

        return
    
    pass

# end