# -*- coding: utf-8 -*-

# Copyright (c) 2004 - 2006 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a canvas widget for an association between two widgets.
"""

from qt import *
from qtcanvas import *

from Arrow import Arrow, NormalArrow, WideArrow

Normal = 0
Generalisation = 1
Imports = 2

NoRegion = 0
West = 1
North = 2
East = 3
South = 4
NorthWest = 5
NorthEast = 6
SouthEast = 7
SouthWest = 8
Center = 9

class AssociationWidget(Arrow):
    """
    Class implementing a canvas widget for an association between two widgets.
    
    The association is drawn as an arrow starting at the first widget and
    ending at the second.
    """
    def __init__(self, canvas, widgetA, widgetB, type = Normal):
        """
        Constructor
        
        @param canvas canvas containing the association (QCanvas)
        @param widgetA first widget of the association
        @param widgetB second widget of the association
        @param type type of the association. This must be one of
            <ul>
            <li>Normal (default)</li>
            <li>Generalisation</li>
            <li>Imports</li>
            </ul>
        """
        if type == Normal:
            arrowType = NormalArrow
            arrowFilled = 1
        elif type == Imports:
            arrowType = NormalArrow
            arrowFilled = 1
        elif type == Generalisation:
            arrowType = WideArrow
            arrowFilled = 0
        
        Arrow.__init__(self, canvas, 0, 0, 100, 100, arrowFilled, arrowType, 1)
        
##        self.calculateEndingPoints = self.calculateEndingPoints_center
        self.calculateEndingPoints = self.calculateEndingPoints_rectangle
        
        self.widgetA = widgetA
        self.widgetB = widgetB
        self.assocType = type
        
        self.regionA = NoRegion
        self.regionB = NoRegion
        
        self.calculateEndingPoints()
        
        self.widgetA.addAssociation(self)
        self.widgetB.addAssociation(self)
        
    def calculateEndingPoints_center(self):
        """
        Method to calculate the ending points of the association widget.
        
        The ending points are calculated from the centers of the
        two associated widgets.
        """
        midA = QPoint(int(self.widgetA.x() + self.widgetA.width() / 2),
                      int(self.widgetA.y() + self.widgetA.height() / 2))
        midB = QPoint(int(self.widgetB.x() + self.widgetB.width() / 2),
                      int(self.widgetB.y() + self.widgetB.height() / 2))
        startP = self.findRectIntersectionPoint(self.widgetA, midA, midB)
        endP = self.findRectIntersectionPoint(self.widgetB, midB, midA)
        
        if startP.x() != -1 and startP.y() != -1 and \
           endP.x() != -1 and endP.y() != -1:
            self.start = (startP.x(), startP.y())
            self.end = (endP.x(), endP.y())
        
        self.update()
        
    def calculateEndingPoints_rectangle(self):
        """
        Method to calculate the ending points of the association widget.
        
        The ending points are calculate by the following method.
        
        For each Widget the diagram is divided in four Regions by its diagonals
        as indicated below
        <pre>
                   \  Region 2  /
                    \          /
                     |--------|
                     | \    / |
                     |  \  /  |
                     |   \/   |
            Region 1 |   /\   | Region 3
                     |  /  \  |
                     | /    \ |
                     |--------|
                    /          \
                   /  Region 4  \
        </pre>
        
        Each diagonal is defined by two corners of the bounding rectangle
        
        To calculate the start point  we have to find out in which
        region (defined by widgetA's diagonals) is widgetB's TopLeft corner 
        (lets call it region M) after that the start point will be
        the middle point of rectangle's side contained in region M.
        
        To calculate the end point we repeat the above  but in the opposite direction
        (from widgetB to widgetA)
        """
        if self.widgetA is None or self.widgetB is None:
            return
            
        xA = int(self.widgetA.x())
        yA = int(self.widgetA.y())
        xB = int(self.widgetB.x())
        yB = int(self.widgetB.y())
        
        # find widgetA region
        rc = QRect(xA, yA, self.widgetA.width(), self.widgetA.height())
        oldRegionA = self.regionA
        self.regionA = self.findPointRegion(rc, xB, yB)
        # move some regions to the standard ones
        if self.regionA == NorthWest:
            self.regionA = North
        elif self.regionA == NorthEast:
            self.regionA = East
        elif self.regionA == SouthEast:
            self.regionA = South
        elif self.regionA == SouthWest:
            self.regionA = West
        elif self.regionA == Center:
            self.regionA = West
            
        self.updateEndPoint(self.regionA, 1)
            
        # now do the same for widgetB
        rc = QRect(xB, yB, self.widgetB.width(), self.widgetB.height())
        oldRegionB = self.regionB
        self.regionB = self.findPointRegion(rc, xA, yA)
        # move some regions to the standard ones
        if self.regionB == NorthWest:
            self.regionB = North
        elif self.regionB == NorthEast:
            self.regionB = East
        elif self.regionB == SouthEast:
            self.regionB = South
        elif self.regionB == SouthWest:
            self.regionB = West
        elif self.regionB == Center:
            self.regionB = West
            
        self.updateEndPoint(self.regionB, 0)
            
        self.update()
        
    def findPointRegion(self, rect, posX, posY):
        """
        Method to find out, which region of rectangle rect contains the point 
        (PosX, PosY) and returns the region number.
        
        @param rect rectangle to calculate the region for
        @param posX x position of point
        @param posY y position of point
        @return the calculated region number<br />
            West = Region 1<br />
            North = Region 2<br />
            East = Region 3<br />
            South = Region 4<br />
            NorthWest = On diagonal 2 between Region 1 and 2<br />
            NorthEast = On diagonal 1 between Region 2 and 3<br />
            SouthEast = On diagonal 2 between Region 3 and 4<br />
            SouthWest = On diagonal 1 between Region4 and 1<br />
            Center = On diagonal 1 and On diagonal 2 (the center)<br />
        """
        w = float(rect.width())
        h = float(rect.height())
        x = float(rect.x())
        y = float(rect.y())
        slope2 = w / h
        slope1 = -slope2
        b1 = x + w / 2.0 - y * slope1
        b2 = x + w / 2.0 - y * slope2
        
        eval1 = slope1 * posY + b1
        eval2 = slope2 * posY + b2
        
        result = NoRegion
        
        # inside region 1
        if eval1 > posX and eval2 > posX:
            result = West
            
        #inside region 2
        elif eval1 > posX and eval2 < posX:
            result = North
            
        # inside region 3
        elif eval1 < posX and eval2 < posX:
            result = East
            
        # inside region 4
        elif eval1 < posX and eval2 > posX:
            result = South
            
        # inside region 5
        elif eval1 == posX and eval2 < posX:
            result = NorthWest
            
        # inside region 6
        elif eval1 < posX and eval2 == posX:
            result = NorthEast
            
        # inside region 7
        elif eval1 == posX and eval2 > posX:
            result = SouthEast
            
        # inside region 8
        elif eval1 > posX and eval2 == posX:
            result = SouthWest
            
        # inside region 9
        elif eval1 == posX and eval2 == posX:
            result = Center
            
        return result
        
    def updateEndPoint(self, region, widgetA):
        """
        Method to update an endpoint.
        
        @param region the region for the endpoint (integer)
        @param widgetA flag indicating update for widgetA is done (boolean)
        """
        if region == NoRegion:
            return
            
        if widgetA:
            widget = self.widgetA
        else:
            widget = self.widgetB
        x = int(widget.x())
        y = int(widget.y())
        ww = widget.width()
        wh = widget.height()
        ch = int(wh / 2)
        cw = int(ww / 2)
        
        if region == West:
            px = x
            py = y + ch
        elif region == North:
            px = x + cw
            py = y
        elif region == East:
            px = x + ww
            py = y + ch
        elif region == South:
            px = x + cw
            py = y + wh
        elif region == Center:
            px = x + cw
            py = y + wh
            
        if widgetA:
            self.start = (px, py)
        else:
            self.end = (px, py)
            
    def findRectIntersectionPoint(self, widget, p1, p2):
        """
        Method to find the intersetion point of a line with a rectangle.
        
        @param widget widget to check against
        @param p1 first point of the line (QPoint)
        @param p2 second point of the line (QPoint)
        @return the intersection point (QPoint)
        """
        result = QPoint(-1, -1)
        old_region = -1
        if widget is None:
            return result
            
        X = -1
        Y = -1
        if widget == self.widgetA:
            X = int(self.widgetB.x())
            Y = int(self.widgetB.y())
        else:
            X = int(self.widgetA.x())
            Y = int(self.widgetA.y())
            
        rc = QRect(int(widget.x()), int(widget.y()), widget.width(), widget.height())
        region = self.findPointRegion(rc, X, Y)
        if region == old_region:
            return QPoint(-1, -1)
            
        if region == West:
            result = self.findIntersection(rc.topLeft(), rc.bottomLeft(), p1, p2)
        elif region == North:
            result = self.findIntersection(rc.topLeft(), rc.topRight(), p1, p2)
        elif region == East:
            result = self.findIntersection(rc.topRight(), rc.bottomRight(), p1, p2)
        elif region == South:
            result = self.findIntersection(rc.bottomLeft(), rc.bottomRight(), p1, p2)
        elif region == NorthWest:
            result = rc.topLeft()
        elif region == NorthEast:
            result = rc.topRight()
        elif region == SouthEast:
            result = rc.bottomRight()
        elif region == SouthWest or region == Center:
            result = rc.bottomLeft()
            
        return result
        
    def findIntersection(self, p1, p2, p3, p4):
        """
        Method to calculate the intersection point of two lines.
        
        The first line is determined by the points p1 and p2, the second
        line by p3 and p4. If the intersection point is not contained in
        the segment p1p2, then it returns (-1, -1).
        
        For the function's internal calculations remember:<br />
        QT coordinates start with the point (0,0) as the topleft corner
        and x-values increase from left to right and y-values increase
        from top to bottom; it means the visible area is quadrant I in
        the regular XY coordinate system
        
        <pre>
            Quadrant II     |   Quadrant I
           -----------------|-----------------
            Quadrant III    |   Quadrant IV
        </pre>
        
        In order for the linear function calculations to work in this method
        we must switch x and y values (x values become y values and viceversa)
        
        @param p1 first point of first line (QPoint)
        @param p2 second point of first line (QPoint)
        @param p3 first point of second line (QPoint)
        @param p4 second point of second line (QPoint)
        @return the intersection point (QPoint)
        """
        x1 = p1.y()
        y1 = p1.x()
        x2 = p2.y()
        y2 = p2.x()
        x3 = p3.y()
        y3 = p3.x()
        x4 = p4.y()
        y4 = p4.x()
        
        # line 1 is the line between (x1, y1) and (x2, y2)
        # line 2 is the line between (x3, y3) and (x4, y4)
        no_line1 = 1    # it is false, if line 1 is a linear function
        no_line2 = 1    # it is false, if line 2 is a linear function
        slope1 = 0.0
        slope2 = 0.0
        b1 = 0.0
        b2 = 0.0
        
        if x2 != x1:
            slope1 = float(y2 - y1) / float(x2 - x1)
            b1 = y1 - slope1 * x1
            no_line1 = 0
        if x4 != x3:
            slope2 = float(y4 - y3) / float(x4 - x3)
            b2 = y3 - slope2 * x3
            no_line2 = 0
            
        pt = QPoint()
        # if either line is not a function
        if no_line1 and no_line2:
            # if the lines are not the same one
            if x1 != x3:
                return QPoint(-1, -1)
            # if the lines are the same ones
            if y3 <= y4:
                if y3 <= y1 and y1 <= y4:
                    return QPoint(y1, x1)
                else:
                    return QPoint(y2, x2)
            else:
                if y4 <= y1 and y1 <= y3:
                    return QPoint(y1, x1)
                else:
                    return QPoint(y2, x2)
                    
        elif no_line1:
            pt.setX(int(slope2 * x1 + b2))
            pt.setY(x1)
            if y1 >= y2:
                if not (y2 <= pt.x() and pt.x() <= y1):
                    pt.setX(-1)
                    pt.setY(-1)
            else:
                if not (y1 <= pt.x() and pt.x() <= y2):
                    pt.setX(-1)
                    pt.setY(-1)
            return pt
        elif no_line2:
            pt.setX(int(slope1 * x3 + b1))
            pt.setY(x3)
            if y3 >= y4:
                if not (y4 <= pt.x() and pt.x() <= y3):
                    pt.setX(-1)
                    pt.setY(-1)
            else:
                if not (y3 <= pt.x() and pt.x() <= y4):
                    pt.setX(-1)
                    pt.setY(-1)
            return pt
            
        if slope1 == slope2:
            pt.setX(-1)
            pt.setY(-1)
            return pt
            
        pt.setY(int((b2 - b1) / (slope1 - slope2)))
        pt.setX(int(slope1 * pt.y() + b1))
        # the intersection point must be inside the segment (x1, y1) (x2, y2)
        if x2 >= x1 and y2 >= y1:
            if not ((x1 <= pt.y() and pt.y() <= x2) and (y1 <= pt.x() and pt.x() <= y2)):
                pt.setX(-1)
                pt.setY(-1)
        elif x2 < x1 and y2 >= y1:
            if not ((x2 <= pt.y() and pt.y() <= x1) and (y1 <= pt.x() and pt.x() <= y2)):
                pt.setX(-1)
                pt.setY(-1)
        elif x2 >= x1 and y2 < y1:
            if not ((x1 <= pt.y() and pt.y() <= x2) and (y2 <= pt.x() and pt.x() <= y1)):
                pt.setX(-1)
                pt.setY(-1)
        else:
            if not ((x2 <= pt.y() and pt.y() <= x1) and (y2 <= pt.x() and pt.x() <= y1)):
                pt.setX(-1)
                pt.setY(-1)
                
        return pt
        
    def widgetMoved(self):
        """
        Method to recalculate the association after a widget was moved.
        """
        self.calculateEndingPoints()
        
    def unassociate(self):
        """
        Method to unassociate from the widgets.
        """
        self.widgetA.removeAssociation(self)
        self.widgetB.removeAssociation(self)
