# This file is part of the Coral project.
# Please read the copyright notice in the LICENSE file.

import coral
import math
import qt
DI = coral.lang.XMIDI20

from coral.modeler.diagram.defaultfont import defaultFont

from coral.modeler.diagram import di_support as DISUPP


def clamp( x, lower, upper ):
    return max(lower,min(x,upper))


class _SizeHinter:
    """The SizeHinter keeps track of which widgets
    have already been layouted, and returns the cached value.
    The framework creates only one SizeHinter instance per
    transaction."""
    def __init__(self):
        self.__sizes = {}

    def getSizeHint(self, layouter, toMinimal = False):
        x = self.__sizes.get(layouter, None)
        if not x:
            x = layouter.sizeHint(self, toMinimal)
            self.__sizes[layouter] = x
        return x

class DiagramElementLayouter:

    def __init__( self, graphElement ):
        self.graphElement = graphElement
        graphElement.layouter = self

        if not self.graphElement.oclIsKindOf(DI.DiagramElement):
            print "DiagramElementLayouter.__init__", self.graphElement

        assert self.graphElement.oclIsKindOf(DI.DiagramElement)

    def contained( self ):
        widget = self.graphElement.widget.__getPyValue__()
        assert widget
        return [w.graphElement.layouter.__getPyValue__() for w in widget.contained()]
        ###return [w.layouter.__getPyValue__() for w in self.graphElement.contained]

    def visibleContained(self):
        w = self.contained()
        return [i for i in w if i.graphElement.isVisible ]

    def isSlave(self):
        """Override this if the layout isn't a slave of its parent."""
        return False

    def padX(self):
        """How much to pad horizontally."""
        return 0

    def padY(self):
        """Same as padX, for the vertical direction."""
        return 0

    def minSize(self):
        """Absolute minimum size for this layout."""
        return 0,0
    
    def sizeHint(self, sizeHinter, toMinimal):
        """Default sizeHint chooses to never shrink nor expand itself."""
        w,h = 0,0

        for c in self.visibleContained():
            _x, _y = c.getPosition()
            _w, _h = sizeHinter.getSizeHint(c, toMinimal)
            w = max(w, _x + _w)
            h = max(h, _y + _h)

        w += self.padX()
        h += self.padY()

        if not self.isSlave() and not toMinimal:
            _w, _h = self.getSize()
            w = max(w, _w)
            h = max(h, _h)

        _w, _h = self.minSize()
        w = max(w, _w)
        h = max(h, _h)
        return w, h

    def layout( self, sizeHinter,
                toMinimal=False , w_parent = -1, h_parent = -1 ):
        w, h = sizeHinter.getSizeHint(self, toMinimal)

        if w_parent > 0:
            w = w_parent
        if h_parent > 0:
            h = h_parent

        self.resize(w,h)
        for c in self.visibleContained():
            c.layout(sizeHinter, toMinimal)

    def move( self, x, y ):
        assert self.graphElement.oclIsKindOf(DI.GraphNode)
        self.graphElement.position.x = x
        self.graphElement.position.y = y

    def getPosition( self ):
        return (float(self.graphElement.position.x),
                float(self.graphElement.position.y))
        
    def resize( self, w, h ):
        assert self.graphElement.oclIsKindOf(DI.GraphNode)
        self.graphElement.size.width = w
        self.graphElement.size.height = h

    def getSize( self ):
        assert self.graphElement.oclIsKindOf(DI.GraphNode)
        return (float(self.graphElement.size.width),
                float(self.graphElement.size.height))

class SlaveLayouter:
    """Convenient superclass for saying we're a slave layouter.
    Remember to superclass this first."""
    def isSlave(self):
        return True



###################################################################
# Basic layouters

class LeafLayout( DiagramElementLayouter ):

    def __init__( self, widget, minw, minh, maxw, maxh ):
        self.minSize = (minw,minh)
        DiagramElementLayouter.__init__(self,widget)

    def sizeHint(self, sizeHinter, toMinimal):
        return self.minSize

class HLineLAY( LeafLayout ):

    def __init__( self, graphElement ):
        LeafLayout.__init__(self, graphElement, 1, 1, 32000, 1)
        
    def resize(self,w,h):
        self.graphElement.size.width = self.graphElement.container.size.width
        self.graphElement.size.height = 1

    def sizeHint(self, sizeHinter, toMinimal):
        return (1,1)


class BoxLayouter( DiagramElementLayouter ):
    def __init__( self, widget ):
        DiagramElementLayouter.__init__(self,widget)

class StringLAY( DiagramElementLayouter ):

    def __init__( self, graphElement ):
        DiagramElementLayouter.__init__(self, graphElement)

    def sizeHint(self, sizeHinter, toMinimal):

        font = defaultFont
        size = DISUPP.get_XMIDI_Property_FontSize(self.graphElement)
        if size:
            font = qt.QFont(font)
            font.setPointSizeFloat(size)

        fm = qt.QFontMetrics(font)
        s = self.graphElement.widget.__getPyValue__().graphElementRenderer.getString()
        ss = s.split("\n")
        w = 0
        for i in ss:
            w = max(w, fm.width(i))
        w += 2 # Because of some QT bug which underestimates the width
        return w, len(ss) * fm.height()



#
# HORIZONTAL LAYOUTING
#
class HBoxLayouterMaster(BoxLayouter):
    def sizeHint(self, sizeHinter, toMinimal):
        w,h = 0,0

        for c in self.visibleContained():
            _w, _h = sizeHinter.getSizeHint(c, toMinimal)
            w += _w
            h = max(h, _h)

        w += self.padX()
        h += self.padY()

        if not self.isSlave() and not toMinimal:
            _w, _h = self.getSize()
            w = max(w, _w)
            h = max(h, _h)

        _w, _h = self.minSize()
        w = max(w, _w)
        h = max(h, _h)
        return w, h

    def layout(self, sizeHinter,
               toMinimal=False, w_parent = -1, h_parent = -1 ):
        w, h = sizeHinter.getSizeHint(self, toMinimal)

        if w_parent > 0:
            w = w_parent
        if h_parent > 0:
            h = h_parent
        
        self.resize(w, h)
        for c in self.visibleContained():
            c.layout(sizeHinter, toMinimal, h_parent = h)
        self.moveContained(sizeHinter)


    def moveContained(self, sizeHinter):
        # Hm, defaults to centering the pads
        x,y = self.padX()/2, self.padY()/2
        for c in self.visibleContained():
            c.move(x,y)
            x += c.getSize()[0]

class HBoxLayouter(SlaveLayouter, HBoxLayouterMaster):
    pass
    
class HBoxLayouterMinChild(HBoxLayouter):
    def layout( self, sizeHinter,
                toMinimal=False, w_parent = -1, h_parent = -1 ):
        w, h = sizeHinter.getSizeHint(self, toMinimal)

        if w_parent > 0:
            w = w_parent
        if h_parent > 0:
            h = h_parent
        
        self.resize(w, h)
        for c in self.visibleContained():
            ## Always minimize children in this layout
            c.layout(sizeHinter, True, h_parent = h)
        self.moveContained(sizeHinter)


#
# VERTICAL LAYOUTING
#
 
class VBoxLayouterMaster(BoxLayouter):
    def sizeHint(self, sizeHinter, toMinimal):
        w,h = 0,0

        for c in self.visibleContained():
            _w, _h = sizeHinter.getSizeHint(c, toMinimal)
            w = max(w, _w)
            h += _h

        w += self.padX()
        h += self.padY()
        if not self.isSlave() and not toMinimal:
            _w, _h = self.getSize()
            w = max(w, _w)
            h = max(h, _h)

        _w, _h = self.minSize()
        w = max(w, _w)
        h = max(h, _h)
        return w,h

    def layout(self, sizeHinter,
               toMinimal=False, w_parent = -1, h_parent = -1 ):
        w, h = sizeHinter.getSizeHint(self, toMinimal)

        if w_parent > 0:
            w = w_parent
        if h_parent > 0:
            h = h_parent

        self.resize(w, h)
        for c in self.visibleContained():
            c.layout(sizeHinter, toMinimal, w_parent = w)
        self.moveContained(sizeHinter)

    def moveContained(self, sizeHinter):
        x = self.padX() / 2
        y = self.padY() / 2
        for c in self.visibleContained():
            c.move(x,y)
            y += c.getSize()[1]

class VCenterBoxLayouterMaster(VBoxLayouterMaster):

    def moveContained(self, sizeHinter):
        heights = self.padY()
        for c in self.visibleContained():
            heights += c.getSize()[1]

        x = self.padX() / 2
        diffY = self.getSize()[1] - heights
        y = diffY / 2
        for c in self.visibleContained():
            c.move(x,y)
            y += c.getSize()[1]

class VBoxLayouterHCenter(VBoxLayouterMaster):
    
    def moveContained(self, sizeHinter):
        x,y= self.padX()/2 , self.padY()/2
        for c in self.visibleContained():
            x=(self.getSize()[0] - c.getSize()[0])/2
            c.move(x,y)
            y+=c.getSize()[1]

    def layout(self, sizeHinter,
               toMinimal=False, w_parent = -1, h_parent = -1 ):
        w, h = sizeHinter.getSizeHint(self, toMinimal)
        if w_parent > 0:
            w = w_parent
        if h_parent > 0:
            h = h_parent
        self.resize(w, h)
        for c in self.visibleContained():
            c.layout(sizeHinter, True)
        self.moveContained(sizeHinter)

class VBoxLayouter(SlaveLayouter, VBoxLayouterMaster):
    pass

class VBoxLayouterPad(VBoxLayouter):
    def padY(self):
        return 4

class VBoxLayouterHCenterPadXY(VBoxLayouterHCenter):
    def padX(self):
        return 6
    
    def padY(self):
        return 6    
#
# EDGE HANDLING
#

class GraphEdgeLayouter( DiagramElementLayouter ):
    def __init__( self, widget ):
        DiagramElementLayouter.__init__(self, widget)

    def layout(self, sizeHinter,
               toMinimal=False, w_parent = -1, h_parent = -1 ):
        """Default layout rearranges the waypoints."""
        for c in self.visibleContained():
            c.layout(sizeHinter, toMinimal)
        self.moveContained(sizeHinter)

    def move(self, x, y):
        """Edges cannot be moved."""
        pass

    def resize(self, w, h):
        """Edges don't need to be resized."""
        pass

    def getSize(self):
        """Edges don't have a size."""
        return 0.0, 0.0

    def sizeHint(self, sizeHinter, toMinimal):
        """Override sizeHint for edges. We don't have a size.
        I wonder if this is bad or surprising behavior?"""
        return 0, 0


    def layoutEdgeAndLabels(self, sizeHinter,
                            connectstart=True, connectend=True ):
        # The waypoints of an edge are relative to the container
        
        # If an edge connects elements from different containers the
        # edge is relative to the container containing the both containers:-).
        
        # If edges are only moved relatively this is not a problem but if we have
        # to calculate waypoint values from scratch the common container has to be known.
        graphEdge=self.graphElement

        if graphEdge.oclIsKindOf(DI.GraphNode):
            return

        if not len(graphEdge.anchor) == 2:
            print "BUG: Attempt to layout a graphEdge (",graphEdge, graphEdge.getName(), graphEdge.semanticModel.element,") with",len(graphEdge.anchor) ,"anchors"
            return

        startconnector=graphEdge.anchor[0]
        endconnector=graphEdge.anchor[1]

        # Find the moved distance by comparing the moved connectors
        # to the waypoints that have not moved
        sX,sY=connectorToEdgeCoordinates(startconnector, graphEdge)
        eX,eY=connectorToEdgeCoordinates(endconnector, graphEdge)
        wsX,wsY=graphEdge.waypoints[0].x,graphEdge.waypoints[0].y
        weX,weY=graphEdge.waypoints[-1].x,graphEdge.waypoints[-1].y
        startDiff=(sX-wsX,sY-wsY)
        endDiff=(eX-weX,eY-weY)

        # If both connectors have moved the same distance then we translate
        # or if it is a self association, only start==end doesn't work.
        #if startconnector.graphElement==endconnector.graphElement:
        #    self.__handleSelfAssociations(startconnector, endconnector)
        if floatEquals(startDiff,endDiff) and not floatEquals(startDiff,(0.0,0.0)):
            self.__translate(startconnector, endconnector)
        else:
            # Move connectors somehow to make the lines straight
            self.slideConnector(startconnector,graphEdge)
            self.slideConnector(endconnector,graphEdge)

            # Connect end points to connectors
            X,Y=connectorToEdgeCoordinates(startconnector, graphEdge)
            if connectstart:
                startDiff=(X-graphEdge.waypoints[0].x,Y-graphEdge.waypoints[0].y)
                graphEdge.waypoints[0].x=X
                graphEdge.waypoints[0].y=Y
            else:
                startDiff = 0.0,0.0

            X,Y=connectorToEdgeCoordinates(endconnector, graphEdge)
            endDiff=(X-graphEdge.waypoints[-1].x,Y-graphEdge.waypoints[-1].y)
            if connectend:
                graphEdge.waypoints[-1].x=X
                graphEdge.waypoints[-1].y=Y
            else:
                endDiff = 0.0,0.0

            self.moveLabels(sizeHinter, startDiff, endDiff)


    def moveContained(self, sizeHinter):
        self.layoutEdgeAndLabels(sizeHinter, True,True)
        

    def __translate(self, startconnector, endconnector):
        elem=startconnector.graphElement
        edge=self.graphElement

        # Find difference between connector and waypoint coordinates
        dx,dy=connectorToEdgeCoordinates(startconnector, edge)
        dx-=edge.waypoints[0].x
        dy-=edge.waypoints[0].y

        # Translate all waypoints by the same offset
        for p in edge.waypoints:
            p.x+=dx
            p.y+=dy
            
        # Translate labels
        for gnode in edge.contained:
            gnode.position.x+=dx
            gnode.position.y+=dy

        # Translate owned graph connectors
        for graphconnector in edge.anchorage:
            graphconnector.position.x+=dx
            graphconnector.position.y+=dy
            

    def moveLabels(self, sizeHinter, startDiff, endDiff):
        """
        If the ends have moved different distances then try to move
        labels relative to their ends..
        """
        edge=self.graphElement

        if len(edge.contained) == 3:
            e0p = edge.contained[0].position
            e0p.position.x += startDiff[0]
            e0p.position.y += startDiff[1]
            
            e1p = edge.contained[1].position
            e1p.x += endDiff[0]
            e1p.y += endDiff[1]

            if len(edge.waypoints) == 2:
                middleDiff = ((startDiff[0]+endDiff[0]) / 2,
                              (startDiff[1]+endDiff[1]) / 2)
                e2p = edge.contained[2].position
                e2p.x += middleDiff[0]
                e2p.y += middleDiff[1]
            

    def slideConnector( self, con, edge ):
        assert len(edge.anchor)==2

        if edge.anchor[0]==con:
            firstwp = edge.waypoints[0]
            secondwp = edge.waypoints[1]
            oppositecon = edge.anchor[1]
        elif edge.anchor[1]==con:
            firstwp = edge.waypoints[-1]
            secondwp = edge.waypoints[-2]
            oppositecon = edge.anchor[0]
        else:
            raise "Can't happen :-( ???"

        # goal point, i.e. the point to which the distance from the connector
        # should be minimized
        # set goal to the position of the second waypoint
        goalx,goaly = coral.XMIDI_LocalToGlobalPosition(edge,float(secondwp.x),float(secondwp.y))

        # special case for edges with 2 waypoints if the opposite connector
        # is a GraphNode
        if len(edge.waypoints)==2 and oppositecon.graphElement.oclIsKindOf(DI.GraphNode):
            # yes, I know this is a bit fugly

            # get the rectangular bounds of the opposite GraphNode
            onode = oppositecon.graphElement
            oleft,otop = coral.XMIDI_LocalToGlobalPosition(onode,0.0,0.0)
            oright = oleft+float(onode.size.width)-1.0
            obottom = otop+float(onode.size.height)-1.0

            # if we have a GraphNode to GraphNode edge
            if con.graphElement.oclIsKindOf(DI.GraphNode):
                # coordinates of first waypoint (the one at con)
                fwpx,fwpy = coral.XMIDI_LocalToGlobalPosition(edge,float(firstwp.x),float(firstwp.y))

                # if the first waypoint is inside in the rectangular bounds of
                # the opposite GraphNode, we have an "inside" edge
                if oleft<fwpx<oright and otop<fwpy<obottom:
                    node = con.graphElement
                    left,top = coral.XMIDI_LocalToGlobalPosition(node,0.0,0.0)
                    right = left+float(node.size.width)-1.0
                    bottom = top+float(node.size.height)-1.0

                    segments = self.__getSegmentsForConnector(oppositecon)
                    fwpx = clamp(fwpx,left,right)
                    fwpy = clamp(fwpy,top,bottom)

                    # set the goal point to the goal point that would have
                    # been chosen from the other connector end, this ensures
                    # correct snapping to the inside of a GraphNode
                    goalx,goaly = self.__getNewConnectorPosition(segments,fwpx,fwpy)
                else:
                    # else, set goal to the midpoint of the two waypoints
                    goalx = (float(firstwp.x) + float(secondwp.x) ) / 2.0
                    goaly = (float(firstwp.y) + float(secondwp.y) ) / 2.0
                    goalx,goaly = coral.XMIDI_LocalToGlobalPosition(edge,goalx,goaly)

            # ensure goalx, goaly are within rectangular bounds of the
            # opposite GraphNode
            goalx = clamp(goalx,oleft,oright)
            goaly = clamp(goaly,otop,obottom)

        elif len(edge.waypoints)==2 and oppositecon.graphElement.oclIsKindOf(DI.GraphEdge):
            goalx = (float(firstwp.x) + float(secondwp.x) ) / 2.0
            goaly = (float(firstwp.y) + float(secondwp.y) ) / 2.0
            goalx,goaly = coral.XMIDI_LocalToGlobalPosition(edge,goalx,goaly)

        segments = self.__getSegmentsForConnector(con)
        conx,cony = self.__getNewConnectorPosition(segments,goalx,goaly)

        con.position.x,con.position.y = coral.XMIDI_GlobalToLocalPosition(con.graphElement,conx,cony)


    def __getNewConnectorPosition( self, segments, goalx, goaly ):
        orthopoints = []
        otherpoints = []
        for (x1,y1,x2,y2) in segments:
            if min(x1,x2)<=goalx<=max(x1,x2) and abs(x1-x2)>0.1:
                x = goalx
                y = y1 + (y2-y1) * (x-x1) / (x2-x1)
                orthopoints.append((x,y))
            elif min(y1,y2)<=goaly<=max(y1,y2) and abs(y1-y2)>0.1:
                y = goaly
                x = x1 + (x2-x1) * (y-y1) / (y2-y1)
                orthopoints.append((x,y))
            else:
                otherpoints.append( ((x1+x2)/2,(y1+y2)/2) )
                otherpoints.append( (x1,y1) )
                otherpoints.append( (x2,y2) )

        # choose the closest orthopoint, or if none, the closest
        # otherpoint
        closest = 1E3000 # = inf
        cx,cy = 0.0, 0.0
        for pts in orthopoints,otherpoints:
            for x,y in pts:
                dist = (x-goalx)**2 + (y-goaly)**2
                if dist<closest:
                    closest = dist
                    cx,cy = x,y
            if closest<1E3000:
                return cx,cy
        return cx,cy
        

    def __getSegmentsForConnector( self, con ):
        """
        Returns a list of line segments (x1,y1,x2,y2) that decribes the
        edges along which the connector con may slide. Currently it returns
        four edges for DI.GraphNodes, and the segments between waypoints for
        DI.GraphEdges. All coordinates should be global.
        
        Since this function returns arbitrary line segments, we could
        make an extension that supports GraphNodes with shapes. This
        could be useful for laying out edges to non-rectangular GraphNodes
        such as UML use cases and packages.
        """
        if con.graphElement.oclIsKindOf(DI.GraphNode):
            node = con.graphElement
            x1,y1 = coral.XMIDI_LocalToGlobalPosition(node,0.0,0.0)
            x2 = x1 + float(node.size.width)-1.0
            y2 = y1 + float(node.size.height)-1.0
            segments = [ (x1,y1,x2,y1),
                         (x1,y1,x1,y2),
                         (x2,y1,x2,y2),
                         (x1,y2,x2,y2) ]
        elif con.graphElement.oclIsKindOf(DI.GraphEdge):
            edge = con.graphElement
            xofs,yofs = coral.XMIDI_LocalToGlobalPosition(edge,0.0,0.0)
            segments = []
            for i in xrange(len(edge.waypoints)-1):
                x1 = float(edge.waypoints[i].x) + xofs
                y1 = float(edge.waypoints[i].y) + yofs
                x2 = float(edge.waypoints[i+1].x) + xofs
                y2 = float(edge.waypoints[i+1].y) + yofs
                segments.append((x1,y1,x2,y2))
        return segments


def connectorToEdgeCoordinates(connector,edge):
    """Convert connector position to the common container offset
       This makes the connector coordinates comparable to the waypoints.
    """
    conn_x,conn_y=coral.XMIDI_LocalToGlobalPosition(connector.graphElement,
                                                    connector.position.x,
                                                    connector.position.y)

    ec_x,ec_y=coral.XMIDI_LocalToGlobalPosition(edge.container,0,0)

    return conn_x - ec_x,conn_y - ec_y

def floatEquals(startDiff, endDiff):
    return _floatEquals(startDiff[0],endDiff[0]) and _floatEquals(startDiff[1],endDiff[1])

def _floatEquals(x,y):
    return abs(x-y)<0.1
