#
#
# Support routines for XMI-DI elements
#
# ABSOLUTELY NO QT / GRAPHICS HERE!
#
#
import math
import sys
import coral
DI = coral.lang.XMIDI20

def findUpwardsModelElement( ge ):

    """Walk upwards in hierarchy to find first model element. """
    s = None
    e = None
    while ge:
        s = None
        if ge.oclIsKindOf(DI.Diagram):
            s = ge.owner
        else:
            s = ge.semanticModel
        if s:
            e = s.element
            if e:
                return e
        ge = ge.container
    return None

def findUpwardsModelElementAndDI( ge ):
    """Walk upwards in hierarchy to find first model element.
    Returns the model element, and the graphelement this has,
    that is a parent to ge (only one possible)."""
    s = None
    e = None
    while ge:
        s = None
        if ge.oclIsKindOf(DI.Diagram):
            s = ge.owner
        else:
            s = ge.semanticModel
        if s:
            e = s.element
            if e:
                return (e, ge)
        ge = ge.container
    return (None, None)

def findSimpleElementOfType( ge, typeInfo ):
    """Returns all ge's GraphElement children (shallowly) with a
    semanticModel of type SimpleSemanticModelElement and with
    the given typeInfo"""
    selem = []
    for c in ge.contained:
        if (c.semanticModel.oclIsKindOf(DI.SimpleSemanticModelElement)
            and unicode(c.semanticModel.typeInfo)==typeInfo):
            selem.append(c)
    # BUG weird?
    if not selem:
        return [ None ]
    return selem


def localToGlobalPosition( ge, x, y ):
    """Maps x,y in GE coordinates (origin at top-left of GE) to
    global coordinates (origin at top-left of world)"""
    return coral.XMIDI_LocalToGlobalPosition(ge, x, y)


def globalToLocalPosition( ge, x, y ):
    """Maps x,y in world coordinates (origin at top-left) to
    GE coordinates (origin at top-left of GE)"""
    return coral.XMIDI_GlobalToLocalPosition(ge, x, y)


def deHasReferentialRole( de ):
    """Marcus' algorithm (I think): Returns True if DiagramElement de's
    semanticBridge is referential. This means that next container
    of de references a semantic element that is not the parent of de's
    referenced semantic element."""
    se = de.semanticModel.element
    if not se:
        return True
    if not de.container or not se.__parent__():
        return False

    sep = se.__parent__()
    de = de.container
    # BUG the de.semanticModel should be removed
    while de:
        if de.semanticModel and de.semanticModel.__hasSlotByName__("element") and not de.semanticModel.element:
            break
        de = de.container
        
    if not de:
        return False
    if de.semanticModel and de.semanticModel.__hasSlotByName__("element") and de.semanticModel.element==sep:
        return False

    return True
    

def find_property_upwards(xelement, propname):
    """Find the Property object of XMIDI Property @propname in @xelement,
    going upwards until found. Returns None if not found."""
    return coral.XMIDI_find_property_upwards(xelement, propname)
    # The following code is much slower than the C++ implementation
    #while element:
    #    for p in element.property:
    #        if p.key == propname:
    #            return p.value
    #    element = element.container
    #return None

def find_either_property_upwards(xelement, propname1, propname2 ):
    """Find the Property object of XMIDI Property @propname1 _or_ @propname2
    in @xelement, going upwards until found. Returns None if not found."""
    return coral.XMIDI_find_either_property_upwards(xelement, propname1, propname2)



def getFgColorValue(xelement):
    """Returns foregound color for XMI-DI @xelement."""
    # "stroke" is Poseidon compatibility
    v = find_either_property_upwards(xelement,
                                     "ForegroundColor",
                                     "stroke")
    if v:
        return str(v.value)
    return "#000000" # 0, 0, 0

def getBgColorValue(xelement):
    """Returns background color for XMI-DI @xelement."""
    # "fill" is Poseidon compatibility
    v = find_either_property_upwards(xelement,
                                     "BackgroundColor",
                                     "fill")
    if v:
        return str(v.value)

    # This is the Rational Rose yello
    #return "#FCFFC2"  # 252,255,194

    # white
    return "#FFFFFF"

def getFontColorValue(xelement):
    """Returns font color for XMI-DI @xelement."""
    # "font-color" is Poseidon compatibility
    v = find_either_property_upwards(xelement,
                                     "FontColor",
                                     "font-color")
    if v:
        return str(v.value)
    return "#000000" # 0,0,0


def calcSegmentRatioFromPointToLine(x1, y1, x2, y2, px, py):
    """Moves px,py towards the line x1,y1-x2,y2 so it forms
    a normal at the meeting point, calculating
    at what "ratio" of the line px,py hit.
    Always returns a floating point. Note that if x1,y1 == x2,y2
    the function returns 0.0."""

    dx = x2 - x1
    dy = y2 - y1
    length = dx * dx + dy * dy
    
    if length == 0:
        return 0.0
    
    u = ( (px-x1) * dx + (py-y1) * dy) / float(length)
    return u

def _calcDistFromPointCoords2Line(x1, y1, x2, y2, px, py, u):
    ## (ipx, ipy) is the intersection point of
    ## the normal to the point from the line
    ipx = x1 + u*(x2-x1)
    ipy = y1 + u*(y2-y1)

    dist = math.sqrt((ipx-px)*(ipx-px)+(ipy-py)*(ipy-py))

    return (ipx, ipy, dist)

def calcDistFromPoint2Line(lpt1, lpt2, p):
    """Calculates distance from @p  to the line given by coordinates
    @lpt1 and @lpt2. They must have .x and .y members.
    Returns triple (ipx, ipy, dist) where (ipx, ipy) define the
    intersection coordinate, and dist is the distance. """
    x1 = float(lpt1.x)
    y1 = float(lpt1.y)
    x2 = float(lpt2.x)
    y2 = float(lpt2.y)
    px = float(p.x)
    py = float(p.y)

    u = calcSegmentRatioFromPointToLine(x1, y1, x2, y2, px, py)
    if (u > 1) or (u < 0):
        return (-1, -1, -1)

    return _calcDistFromPointCoords2Line(x1, y1, x2, y2, px, py, u)

def calcRatioFromPoint2Lines(waypoints, p):
    """Calculates at what percentage @p is closest to any
    of the waypoints or their line segments.
    Return values are
      1. 0..1 for the ratio
      2. a tuple (x, y) for the 'hit position'
      3. the distance from p to that hit position
    This routine can be used to e.g. move edge labels
    'appropriately' when waypoints are changed.
    """
    mindist = None
    mindist_position = None
    mindist_length_from_beginning = None
    length_so_far = 0.0
    for idx in range(len(waypoints)):
        lp1 = waypoints[idx]
        if idx < len(waypoints)-1:
            lp2 = waypoints[idx+1]
        else:
            lp2 = lp1
        x1 = float(lp1.x)
        y1 = float(lp1.y)
        x2 = float(lp2.x)
        y2 = float(lp2.y)
        px = float(p.x)
        py = float(p.y)
        u = calcSegmentRatioFromPointToLine(x1, y1, x2, y2, px, py)
        if (u > 1) or (u < 0):
            # If outside bounds, just calculate the distance to the
            # current point
            cx, cy = lp1.x, lp1.y
            _dx = p.x - lp1.x
            _dy = p.y - lp1.y
            dist = math.sqrt(_dx * _dx + _dy * _dy)
            u = 0.0
        else:
            cx, cy, dist = _calcDistFromPointCoords2Line(x1, y1, x2, y2, px, py, u)
        assert dist >= 0.0

        _dx = lp1.x - lp2.x
        _dy = lp1.y - lp2.y
        segment_length = math.sqrt(_dx * _dx + _dy * _dy)

        if mindist is None or mindist > dist:
            mindist = dist
            mindist_position = (cx, cy)
            mindist_length_from_beginning = length_so_far + u * segment_length

        length_so_far += segment_length

    mindist_ratio = mindist_length_from_beginning / float(length_so_far)
    print "RETURNING RATIO", mindist_ratio, mindist_position, mindist
    return mindist_ratio, mindist_position, mindist

def calcPointAtWaypoints(waypoints, ratio):
    """Calculates a specific point p along waypoints such that
    the distance from the first waypoint to p divided by the total
    length is equal to ratio. E.g. setting ratio=0.5 returns the coordinates
    at the middle of the line.

    Returns tuple (x, y, direction) where direction is the angle in radians
    of the line segment.
    """
    assert ratio >= 0.0
    assert ratio <= 1.0
    assert len(waypoints)
    total_length = 0.0
    last = waypoints[-1]
    for i in xrange(len(waypoints)):
        p = waypoints[i]
        if p == last:
            continue
        p2 = waypoints[i+1]
        dx = p2.x - p.x
        dy = p2.y - p.y
        l = math.sqrt(dx * dx + dy * dy)
        total_length += l
    if total_length == 0.0:
        return (waypoints[0].x, waypoints[0].x, 0)

    target_len = total_length * ratio
    cur_len = 0.0
    for i in xrange(len(waypoints)):
        p = waypoints[i]
        if p == last:
            continue
        p2 = waypoints[i+1]
        dx = p2.x - p.x
        dy = p2.y - p.y
        l = math.sqrt(dx * dx + dy * dy)
        if cur_len + l >= target_len:
            direction = math.atan2(dy, dx)
            rest = target_len - cur_len
            cx = p.x
            cy = p.y
            cx += math.cos(direction) * rest
            cy += math.sin(direction) * rest
            return cx, cy, direction
        else:
            cur_len += l
    assert 0

def calcGENameDIPoints(waypointlist):
    """This calculates a point close to the middle of a waypoint list.
    It is used for positioning e.g. Association Names."""
    x,y,d = calcPointAtWaypoints(waypointlist, 0.5)
    return DI.Point(x = x, y = y)

def findDiagramOfElement(xmidielement):
    diagram = xmidielement
    while diagram and not diagram.oclIsKindOf(DI.Diagram):
        diagram = diagram.__parent__()
    return diagram


def calculateXMIDIBoundingBox(xmidielements):
    """Calculates the bounding box of the given visible XMI-DI elements
    and their children. Returns a 4-tuple (minx, miny, maxx, maxy).
    The width and height can be retrieve with maxx-minx and maxy-miny."""
    sentinel = sys.maxint
    minx, miny = sentinel, sentinel
    maxx, maxy = -sentinel, -sentinel
    for e in xmidielements:
        if not e.isVisible:
            continue
        if e.oclIsKindOf(DI.GraphNode):
            pos = e.position
            x = float(pos.x)
            y = float(pos.y)
            lx = [ x ]
            ly = [ y ]
            size = e.size
            if e.oclIsKindOf(DI.Diagram):
                lx = []
                ly = []
                size = None
            if size:
                lmx = [ size.width + x ]
                lmy = [ size.height + y ]
            else:
                # XMI-DI spec 8.8, Size.
                _minx, _miny, _maxx, _maxy = calculateXMIDIBoundingBox(e.contained)
                _minx += x
                _miny += y
                _maxx += x
                _maxy += y
                lx.append(_minx)
                ly.append(_miny)
                lmx = [ _maxx ]
                lmy = [ _maxy ]
                
        elif e.oclIsKindOf(DI.GraphEdge):
            # ignore a GraphEdges position, only its waypoints matter
            lx = []
            ly = []
            lmx = []
            lmy = []
            for p in e.waypoints:
                # Add some space around, for the arrow
                x = p.x
                y = p.y
                lx.append(x - 5)
                ly.append(y - 5)
                lmx.append(x + 5)
                lmy.append(y + 5)

        else:
            continue

        for x in lx:
            if x < minx: minx = x
        for y in ly:
            if y < miny: miny = y
        for mx in lmx:
            if mx > maxx: maxx = mx
        for my in lmy:
            if my > maxy: maxy = my

    # Empty diagram?
    if minx >= sentinel or miny >= sentinel or maxx <= -sentinel or maxy <= -sentinel:
        minx, miny, maxx, maxy = 0, 0, 0, 0

    return (minx, miny, maxx, maxy)

def getCommonParent(elem1, elem2):
    """Returns the lowest common parent of @elem1 and @elem2.
    Returns None if no such parent exists.
    Note that this function can never return @elem1 or @elem2."""
    parents1 = coral.Element__getMyselfAndParents__(elem1, False)
    parents2 = coral.Element__getMyselfAndParents__(elem2, False)
    for i in parents1:
        if i in parents2:
            return i
    for i in parents2:
        if i in parents1:
            return i
    return None

def __shift(graphelement, shift_x, shift_y):
    for ge in graphelement.contained:
        if ge.oclIsKindOf(DI.GraphNode):
            ge.position.x += shift_x
            ge.position.y += shift_y
        elif ge.oclIsKindOf(DI.GraphEdge):
            for p in ge.waypoints:
                p.x += shift_x
                p.y += shift_y
            __shift(ge, shift_x, shift_y)
    
def possiblyMoveDiagramContents(element_w_position_or_size):
    """If a top-level graphnode moves to the left or top edge,
    shift the contents of the whole canvas."""
    if not element_w_position_or_size.oclIsKindOf(DI.GraphNode):
        return

    diagram = element_w_position_or_size.__parent__()
    if not diagram or not diagram.oclIsKindOf(DI.Diagram):
        return

    # small BUG, I think we should calculate a size for the diagram
    # here.
    if not diagram.size:
        return
    
    shift_x = 0
    shift_y = 0
    shift_w = 0
    shift_h = 0

    # The magic is that we check the *current* values
    # of .x and .y, no matter how many transactions have
    # been done on them
    x = element_w_position_or_size.position.x
    y = element_w_position_or_size.position.y

    w = element_w_position_or_size.size.width
    h = element_w_position_or_size.size.height

    shift_atleast = 100
    
    if x <= 0:
        shift_x = shift_atleast - x
    if y <= 0:
        shift_y = shift_atleast - y

    if x + w >= diagram.size.width - 1:
        shift_w = shift_atleast + x + w - diagram.size.width
    if y + h >= diagram.size.height - 1:
        shift_h = shift_atleast + y + h - diagram.size.height

    __shift(diagram, shift_x, shift_y)

    # Finally make the diagram bigger
    diagram.size.width += shift_x + shift_w
    diagram.size.height += shift_y + shift_h


def getAllLinkedProjects(allP):
    """Returns list of all projects which the elements in @allP
    point to, transitively."""
    k = dict.fromkeys(allP)
    new_projects = {}
    new_projects.update(k)
    all_elems = {}
    while new_projects:
        for p in new_projects.keys():
            del new_projects[p]
            for root in p.contents():
                all_root_elems = root.__getAllParts__()
                for e in all_root_elems:
                    all_elems[e] = None
                for e in all_root_elems:
                    for slot in e.__all_slots__():
                        prop = slot.getProperty()
                        if prop.getTarget().isDatatypeDefinition():
                            continue
                        if prop.isComposition():
                            continue
                        if prop.isAnonymous():
                            continue
                        if prop.isUnserializable():
                            continue
                        for child in slot:
                            try:
                                all_elems[child]
                            except KeyError:
                                all_elems[child] = None
                                child_prj = child.__getRootsProject__()
                                if child_prj is p:
                                    continue
                                if k.has_key(child_prj):
                                    continue
                                new_projects[child_prj] = None
                                k[child_prj] = None

    # preserve order in allP
    a = []
    for p in allP:
        a.append(p)
    # Populate with new projects
    for p in k.keys():
        if p not in a:
            a.append(p)
            
    return a
                            

def get_XMIDI_Property_FontSize(ge):
    p = find_property_upwards(ge, "FontSize")
    if not p:
        return None
    try:
        v = float(str(p.value))
    except ValueError:
        return None
    if v <= 0.0:
        return None
    return v
