// MEWGanttLinesController completes the given ganttElement with gantt lines.
// The server generates markup which describes the connection between bars, but does not generate the
// actual connection lines. This is because the vertical layout is completely done by the browser.
// Hence the server has no knowledge about the exact end positions of the bars.
// The gantt lines are generated and placed inside the divs with class == "lines".
//
// The ganttElement markup is like this:
//
//      <div class="content_area">
//          <div class="gantt">
//
//              <table class="rows">
//                  <tr id="row1" class="row">
//                      ...
//                  </tr>
//                  <tr id="row2" class="row">
//                      ...
//                  </tr>
//              </table>
//
//              <svg class="lines" version="1.1" xmlns="http://www.w3.org/2000/svg">
//                  <g class="line" data-start-id="row1" data-end-id="row2" data-type="ES"></g>
//                                  ...
//              </svg>
//
//          </div>
//      </div>
//
// The style values like border-width, color and style are also generated by the server using CSS.
// Those style values are attached to the "line" div. MEWGanttLinesController uses them for the dynamically generated path segments
// and removes them afterwards from the "line" div, because otherwise the "line" div, which actually is an invisible
// container for the path segments, would show its border.

MEWGanttLinesController = new Class({
    
	hDistanceMultiplicator: 0.65,
    
	SVGNamespace: "http://www.w3.org/2000/svg",
    
	showsCrispEdges: false,
    
	addsOffsetsToMakeLinesCrisp: true,
    
	initialize: function(ganttElement, timeScaleElement, ganttDocument, componentID){
	    this.ganttDocument = ganttDocument;
	    this.ganttElement = ganttElement;
		this.ganttLines = this.ganttElement.getElements('.ganttLine');
	    this.linesElement = this.ganttElement.getElements('.ganttLines')[0];
	    this.timeScaleElement = timeScaleElement;

	    Array.each(this.ganttLines, function(lineContainer){
			this.cacheLineContainerInvariants(lineContainer);
		}, this);    
	
	    // The clip data text field is always inside the main document and not inside an iframe:
	    // This needs to be set before calling updateStartAndEndMargins();
	    this.clipDataTextField = document.getElementById(componentID+'-CD');
    
	    this.updateStartAndEndMargins();
	    this.updateMiddleLabels();
				
        // Do nothing if we do not have any gantt lines
	    if(ganttElement && this.linesElement)
	        this.createLines();
	},
    
	cacheLineContainerInvariants: function(lineContainer){		

        // The line type must be one of the following:
        // "ES"   ->   End to Start
        // "SE"   ->   Start to End
        // "SS"   ->   Start to Start
        // "EE"   ->   End to End

		lineContainer.lineType = lineContainer.getAttribute('data-type');    
		lineContainer.startID  = lineContainer.getAttribute('data-start-id');
		lineContainer.endID	   = lineContainer.getAttribute('data-end-id');				

		// Save the line style attributes.
		lineContainer.lineStyles = this.stylesFromLineContainer(lineContainer);
	},
		
	    // Fetches the style data, bar positions and bar sizes from the each line container description:
	    // Returns only valid lineContainers which have a start and an end row.
	fetchBarPositions: function(lineContainers){

		// var watch = new PWStopWatch(true);
		// this.beamMeasuresWatch = new PWStopWatch();
		// this.beamMeasuresMilliseconds = 0;
		// this.rowCoordinatesWatch = new PWStopWatch();
		// this.rowCoordinatesMilliseconds = 0;
		//
		// var invariantsWatch = new PWStopWatch(true);
		
		// Loop invariants
		var position = this.ganttElement.getBoundingClientRect(); 
		this.ganttElementOffsets = {x: position.left, y: position.top};

		// var invariantsMilliseconds = invariantsWatch.duration();

	    var validLineContainers = [];
	    Array.each(lineContainers, function(lineContainer){
        
	        // Get the start and end rows:
	        var startRow = this.startRowForLineContainer(lineContainer);
	        var endRow   = this.endRowForLineContainer(lineContainer);
			
	        if(startRow && endRow && lineContainer.startBeam && lineContainer.endBeam){
	            // Save the bar coordinates and link distances for start and end bars.
	            lineContainer.beamMeasures = this.beamsMeasures(lineContainer.startBeam, lineContainer.endBeam);
	            validLineContainers.push(lineContainer);
	        }
	    }, this);

		// var duration = watch.duration();
		// console.log('-----------------------------');
		// console.log('fetchBarPositions          : '+duration);
		// console.log('davon rowCoordinatesForBar : '+this.rowCoordinatesMilliseconds);
		// console.log('davon beamMeasures         : '+this.beamMeasuresMilliseconds);
		// console.log('davon Invarianten berechnen: '+invariantsMilliseconds);
		// console.log('Differenz                  : '+(duration-this.rowCoordinatesMilliseconds-this.beamMeasuresMilliseconds-invariantsMilliseconds));
				
	    return validLineContainers;
	},
    
	// Returns the visible start row and caches it 
	startRowForLineContainer: function(lineContainer){
		var row;// = lineContainer.startRow;
//		if(!row)
        {
			row = this.ganttDocument.body.getElement('.gantt .row#'+lineContainer.startID);
			if(!row) // Row is not visible
				return null;

			lineContainer.startRow = row;

            // Get the start beam. A beam is the rectangular piece inside a gantt row. It could either be the used for a bar,
            // or inside a group to draw the bar.
            // I use the term "beam" because "bar" is already used to to discriminate between a bar row and a group row.
            lineContainer.startBeam = row.getElement('.bar');
            var startRhombContainer = lineContainer.startBeam ? lineContainer.startBeam.getParent('.rhombContainer') : null;
            if(startRhombContainer) {
                // We use the rhombContainer instead of the rhomb itself, because in webkit browsers, the position of the rhomb
                // is not available at this time. The size of the rhombContainer depends on the font size, just as the size of the rhomb.
                // So this should work as well.
                lineContainer.startBeam = startRhombContainer;
            }
		}
		return row.isVisible() ? row : null;
	},

	// Returns the visible end row and caches it 
	endRowForLineContainer: function(lineContainer){
		var row;// = lineContainer.endRow;
//		if(!row)
        {
			row = this.ganttDocument.body.getElement('.gantt .row#'+lineContainer.endID);
			if(!row) // Row is not visible
				return null;

			lineContainer.endRow = row;
            lineContainer.endBeam = row.getElement('.bar');
            var endRhombContainer = lineContainer.endBeam ? lineContainer.endBeam.getParent('.rhombContainer') : null;
            if(endRhombContainer)
                lineContainer.endBeam = endRhombContainer;			
		}
		return row.isVisible() ? row : null;
	},
		
	    // Returns the coordinates of the row in which a bar lives.
	rowCoordinatesForBar: function(bar) {

		// DEBUG
		// this.rowCoordinatesWatch.reset();
		
		var row = this.parentRowForBar(bar);		
		var r = row.getBoundingClientRect();
		var result = { 'origin':{x:r.left, y:(r.top-this.ganttElementOffsets.y)}, 'size':{y:(r.bottom-r.top)}};

		// DEBUG		
		// this.rowCoordinatesMilliseconds += this.rowCoordinatesWatch.duration();
		
		return result;
	},
	
	parentRowForBar: function(bar){
		var row = bar.row;
		if(!row){
			row = bar.getParent('.row');
			bar.row = row;
		}
		return row;
	},
	
	
	updateLines: function(){
        // Do nothing if we do not have any gantt lines
        if(this.linesElement){
            this.removeLines();
            this.createLines();
        }
	},
    
	    // Remove all gantt lines. Is used together with createLines to refresh all gantt lines.
	removeLines: function() {
	    var lines = this.linesElement;
	    var childs = lines.getChildren ? lines.getChildren() : lines.children; // In firefox getChildren() is not defined.
	    Array.each(childs, function(lineContainer){
	        while (lineContainer.firstChild) {
	            lineContainer.removeChild(lineContainer.firstChild);
	        }
	    });
	},
    
	    // Creates all gantt lines.
	createLines: function(){
	    //		this.linesElement.setStyle('opacity', 0);
	    // The mootools method setStyle is not available in Firefox for SVG elements.
	    // Therefore I use the native style property directly.
	    // But setting the display to 'none' and afterwards to 'block' is slower in FF.
	    this.linesElement.style['display'] = 'none';
    
	    // 1. Attach bar positions to line containers:
	    var lineContainers = this.fetchBarPositions(this.ganttLines);
    	
	    // 2. Create lines:
	    var gantt = this;
	    Array.each(lineContainers, function(lineContainer){
	        gantt.addLinesToContainer(lineContainer);
	    });
    
	    // Note: It is important to hide the svg element if it contains no elements,
	    // because otherwise it would grab mouse events and does not forward them to other elements.
	    if(lineContainers.length > 0)
	        this.linesElement.style['display'] = 'block';

	},
    
	    // Adds an SVG path representing a gantt line to lineContainer.
	addLinesToContainer: function(lineContainer){
	    var startBeam   = lineContainer.startBeam;
	    var endBeam     = lineContainer.endBeam;
	    switch (lineContainer.lineType) {
	        case 'ES': this.addLinesConnectedToMixedBarEnds(lineContainer, startBeam, endBeam, 'ES'); break;
	        case 'SE': this.addLinesConnectedToMixedBarEnds(lineContainer, startBeam, endBeam, 'SE'); break;
	        case 'SS': this.addStartToStartLines(lineContainer, startBeam, endBeam); break;
	        case 'EE': this.addEndToEndLines(lineContainer, startBeam, endBeam); break;
	    }
	},
    
	    // Returns a dictionary with style attributes fetched from lineContainer.
	stylesFromLineContainer: function(lineContainer){
		if(!lineContainer.computedStyles){
		
		    var style = window.getComputedStyle(lineContainer);
    
		    // It does not matter which side we get the border width, style and color from.
		    // At start all sides have the same border values.
		    // Here we take it from the left side.
		    //
		    // Note: It looks like Webkit does round down fractional values: 0.5 becomes 0.0!
		    var lw = style.borderLeftWidth.toFloat();
		    if(lw == 0)
		        lw = 1.0;
    
		    var result = {
		    lineWidth:    lw,
		    style:        style.borderLeftStyle,
		    color:        style.borderLeftColor,
		    borderRadius: 3 // Could be style value in the future
		    };
    
		    if(PWBrowserIsFirefox()) {
		        // Firefox does not give us the "real" computed style:
		        // The returned object actually represents the CSS 2.1 used values, not the computed values.
		        // Originally, CSS 2.0 defined the computed values to be the "ready to be used" values of properties after cascading and inheritance,
		        // but CSS 2.1 redefined computed values as pre-layout, and used values as post-layout. The getComputedStyle function returns the old
		        // meaning of computed values, now called used values. There is no DOM API to get CSS 2.1 computed values.
        
		        // Instead take the values from the element style if possible:
		        var elementStyle = lineContainer.style;
		        var lineWidth = elementStyle.borderWidth;
		        var lineStyle = elementStyle.style;
		        var lineColor = elementStyle.color;
		        if(lineWidth)
		            result.lineWidth = lineWidth.toFloat();
		        if(lineStyle)
		            result.style = lineStyle;
		        if(lineColor)
		            result.color = lineColor;
		    }
			lineContainer.computedStyles = result
		}
	    return lineContainer.computedStyles;
	},
    
	beamMeasures: function(beam)
	{
	    if(!beam)
	        return {};

		// DEBUG
		// this.beamMeasuresWatch.reset();

		var position = beam.getBoundingClientRect();
		var origin = {x: position.left - this.ganttElementOffsets.x , y: position.top - this.ganttElementOffsets.y};
	
		// TEST Cache
		if(!beam.pwSize)
		{		
	    	beam.pwSize = { width:beam.offsetWidth, height:beam.offsetHeight };
			beam.originX = origin.x;

		    // Note: This is the place where you can adjust the start and end positions of gantt lines
		    //       to fit to special geometry like milestones.
    
		    // In order to get the right vertical position of a milestone we need to ask the
		    // rhomb container for its position. But the width of the rhomb container is 0px.
		    // (The milestone is drawn outside the rhomb container). So we need to adjust the horizontal
		    // position and size to match the visible milestone.
		    if(beam.hasClass('rhombContainer'))
		    {
		        var bar       = beam.getChildren('.middleLabel .rhomb .bar')[0];				
		        var width     = bar.offsetWidth * Math.sqrt(2);  // Multiply with sqrt(2) because the rhomb is rotated by 45 degree.
		        beam.originX -= width / 2.0;
		        beam.pwSize   = {width:width, height:beam.pwSize.height};
				
			}
		}
       
		// DEBUG		
		// this.beamMeasuresMilliseconds += this.beamMeasuresWatch.duration();
	   
	   
	    return { origin:{x:beam.originX, y:origin.y}, size:beam.pwSize };
	},

	    // Returns the beam coordinates and link distances for start and end bars.
	    // Used in drawing methods.
	beamsMeasures: function(startBeam, endBeam){
		var distance = 10.0;
	 	 return {      start:this.beamMeasures(startBeam), 
			             end:this.beamMeasures(endBeam),
	 	  hDistanceFromStart:distance,
		    hDistanceFromEnd:distance,
	   endBeamRowCoordinates:this.rowCoordinatesForBar(endBeam),
	 startBeamRowCoordinates:this.rowCoordinatesForBar(startBeam)};
	},
        
	    // Returns the arrow size for a given line width.
	arrowSizeForLineWidth: function (lineWidth) {
	    var length = 5.0 + (lineWidth-1.0);
	    return { width:length, height:length };
	},
    
	    // Returns an empty SVG path with the given line width and color.
	emptySVGPath: function (lineWidth, color) {
	    var result = this.ganttDocument.createElementNS(this.SVGNamespace, "path");
	    result.setAttribute('style', 'fill:none; stroke:'+color+'; stroke-width:'+lineWidth+';');
	    if(this.showsCrispEdges)
	        result.setAttribute('shape-rendering', 'crispEdges');
	    return result;
	},
    
	    // Returns an SGV path representing an arrow tipping at point in the given direction and with the given line width and color.
	    // Direction can be: 'left', 'right', 'up' or 'down'.
	svgArrow: function (tipPoint, direction, lineWidth, color) {
	    var arrowLength = this.arrowSizeForLineWidth(lineWidth).width;
	    var arrow = this.ganttDocument.createElementNS(this.SVGNamespace, "polygon");
	    arrow.setAttribute('style', 'fill:'+color);
	    if(this.showsCrispEdges)
	        arrow.setAttribute('shape-rendering', 'crispEdges');
	    var halfArrowLength = arrowLength / 2.0;
	    switch (direction) {
	        case 'right':
	            arrow.setAttribute('points', ''+tipPoint.x+','+tipPoint.y+' '+(tipPoint.x-arrowLength)+','+(tipPoint.y-halfArrowLength)+' '+(tipPoint.x-arrowLength)+','+(tipPoint.y+halfArrowLength));
	            break;
	        case 'left':
	            arrow.setAttribute('points', ''+tipPoint.x+','+ tipPoint.y+' '+(tipPoint.x+arrowLength)+','+(tipPoint.y-halfArrowLength)+' '+( tipPoint.x+arrowLength)+','+(tipPoint.y+halfArrowLength));
	            break;
	        case 'up':
	            arrow.setAttribute('points', ''+tipPoint.x+','+tipPoint.y+' '+(tipPoint.x-halfArrowLength)+','+(tipPoint.y+arrowLength)+' '+(tipPoint.x+halfArrowLength)+','+(tipPoint.y+arrowLength));
	            break;
	        case 'down':
	            arrow.setAttribute('points', ''+tipPoint.x+','+tipPoint.y+' '+(tipPoint.x-halfArrowLength)+','+(tipPoint.y-arrowLength)+' '+(tipPoint.x+halfArrowLength)+','+(tipPoint.y-arrowLength));
	            break;
	    }
    
	    return arrow;
	},
    
	    // Creates and adds gantt lines to lineContainer for "start to end" and "end to start" connections.
	addLinesConnectedToMixedBarEnds: function(lineContainer, startBeam, endBeam, type){
	    var arrowAtEnd;
	    var sv = lineContainer.lineStyles;
	    var bm = lineContainer.beamMeasures;
    
	    if(!bm.start.origin && !bm.end.origin)
	        return;
    
	    if(type=="ES")
	        arrowAtEnd = true;
	    else{
	        arrowAtEnd = false;
	        var bar = startBeam;
	        startBeam = endBeam;
	        endBeam = bar;
        
	        var start = bm.start;
	        bm.start = bm.end;
	        bm.end = start;
        
	        start = bm.hDistanceFromStart;
	        bm.hDistanceFromStart = bm.hDistanceFromEnd;
	        bm.hDistanceFromEnd = start;
	    }
    
	    var arrowLength = this.arrowSizeForLineWidth(sv.lineWidth).width;
	    var addOffset = this.addsOffsetsToMakeLinesCrisp;
	    var offset = addOffset ? (sv.lineWidth % 2 === 0 ? 0.0 : 0.5) : 0.0;
    
	    // Points are named after the document "GanttLine.graffle" written by Torsten
	    var S = { x:bm.start.origin.x+bm.start.size.width-1.0,  y:bm.start.origin.y+(bm.start.size.height/2.0)};
	    if(addOffset)
	        S.y = Math.round(S.y)+offset;
	    var E = { x:bm.end.origin.x,                            y:bm.end.origin.y+(bm.end.size.height/2.0)};
	    if(addOffset)
	        E.y = Math.round(E.y)+offset;
	    var A = { x:S.x+bm.hDistanceFromStart,                  y:S.y};
	    if(addOffset)
	        A.x = Math.round(A.x)+offset;
	    var D = { x:E.x-bm.hDistanceFromEnd,                    y:E.y};
	    if(addOffset)
	        D.x = Math.round(D.x)+offset;
    
    
	    // If the horizontal space between the two vertical lines is to small to be connected with arcs,
	    // directly connect the lines. In order to do so, move them slightly.
	    var distance = Math.abs(D.x - A.x);
	    if(distance <= (sv.borderRadius * 2))
	    {
	        A.x = D.x;
	    }
    
	    var HStartTop    = { x:bm.start.origin.x+(bm.start.size.width/2.0), y:bm.start.origin.y};
	    var HStartBottom = { x:HStartTop.x,                                 y:bm.start.origin.y+bm.start.size.height};
	    var HEndTop      = { x:bm.end.origin.x+(bm.end.size.width/2.0),     y:bm.end.origin.y};
	    var HEndBottom   = { x:HEndTop.x,                                   y:bm.end.origin.y+bm.end.size.height};
    
	    var startBeamIsOnTop = bm.start.origin.y < bm.end.origin.y;
    
	    var me = this;
	    if(D.x < A.x) {
            // TODO AK@TR ask where the code in Merlin is and why I need to offset both start and end by 1.5 pixel
            // to get the same result as in Merlin.
            if(S.x+1.5 > E.x-1.5 && A.x < HEndTop.x){
	            if(type=='ES'){
	                // Arrow points to top or to bottom:
	                //
	                //      S---S1-A
	                //             |
	                //            A1     HTop
	                //      E ===========|
                
	                var tipPoint = startBeamIsOnTop ? HEndTop : HEndBottom;
	                tipPoint.x = A.x;
	                var direction = startBeamIsOnTop ? 'down' : 'up';
	                var A1 = {x:tipPoint.x, y:tipPoint.y + (startBeamIsOnTop ? -arrowLength : arrowLength)};
	                var S1 = {x:A.x-sv.borderRadius, y:A.y};
	                var path = this.emptySVGPath(sv.lineWidth, sv.color);
	                path.setAttribute('d',   'M'+S.x+' '+S.y
	                    + ' L'+S1.x+' '+S1.y
	                    + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 1 : 0)+' '+sv.borderRadius+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	                    + ' L'+A1.x+' '+A1.y);
	                lineContainer.appendChild(path);
	                A1.y += startBeamIsOnTop ? arrowLength : -arrowLength;
	                lineContainer.appendChild(this.svgArrow(A1, direction, sv.lineWidth, sv.color));
                
	            }else{
	                // Arrow points to top or to bottom:
	                //
	                //       ===========|
	                //            D1    HBottom
	                //             |
	                //             D-E1--E
                
                
	                var tipPoint = startBeamIsOnTop ? HStartBottom : HStartTop;
	                tipPoint.x = D.x;
	                var direction = startBeamIsOnTop ? 'up' : 'down';
	                var D1 = {x:tipPoint.x, y:tipPoint.y + (startBeamIsOnTop ? arrowLength : -arrowLength)};
	                var E1 = {x:D.x+sv.borderRadius, y:E.y};
	                var path = this.emptySVGPath(sv.lineWidth, sv.color);
	                path.setAttribute('d',   'M'+E.x+' '+E.y
	                    + ' L'+E1.x+' '+E1.y
	                    + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 1 : 0)+' '+(-sv.borderRadius)+' '+(startBeamIsOnTop ? -sv.borderRadius : sv.borderRadius)+' '
	                    + ' L'+D1.x+' '+D1.y
	                    );
	                lineContainer.appendChild(path);
	                D1.y += startBeamIsOnTop ? -arrowLength : arrowLength;
	                lineContainer.appendChild(this.svgArrow(D1, direction, sv.lineWidth, sv.color));
	            }
	        }else{
	            // ZigZag line:             S---S1-A               D----E
	            //                                 |               |
	            //                                 |               C1
	            //                                 A1              |
	            //                                 |               |
	            //                  C-B1-----------B       or      C-B1------------B
	            //                  |                                              |
	            //                  |                                              A1
	            //                  C1                                             |
	            //                  |                                              |
	            //                  D----E                                   S--S1-A
            
	            var endRowCoordinates   = bm.endBeamRowCoordinates;
	            var startRowCoordinates = bm.startBeamRowCoordinates;
			
	            var B;
	            if(arrowAtEnd){
	                E.x -= arrowLength; // Make room for the arrow
	                B = { x:A.x, y:startBeamIsOnTop ? endRowCoordinates.origin.y : endRowCoordinates.origin.y+endRowCoordinates.size.y};
	            }else{
	                S.x += arrowLength;  // Make room for the arrow
	                B = { x:A.x, y:startBeamIsOnTop ? startRowCoordinates.origin.y+startRowCoordinates.size.y : startRowCoordinates.origin.y};
	            }
	            if(addOffset)
	                B.y = Math.round(B.y)+offset;
	            var C  = { x:D.x, y:B.y};
	            var S1 = { x:A.x-sv.borderRadius, y:A.y };
	            var A1 = { x:A.x, y: startBeamIsOnTop ?  B.y-sv.borderRadius : B.y+sv.borderRadius };
	            var B1 = { x:C.x+sv.borderRadius, y:B.y};
	            var C1 = { x:D.x, y: startBeamIsOnTop ?  D.y-sv.borderRadius : D.y+sv.borderRadius};
            
	            var path = this.emptySVGPath(sv.lineWidth, sv.color);
	            path.setAttribute('d',   'M'+S.x+' '+S.y
	                + ' L'+S1.x+' '+S1.y
	                + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 1 : 0)+' '+sv.borderRadius+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	                + ' L'+A1.x+' '+A1.y
	                + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 1 : 0)+' '+(-sv.borderRadius)+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	                + ' L'+B1.x+' '+B1.y
	                + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 0 : 1)+' '+(-sv.borderRadius)+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	                + ' L'+C1.x+' '+C1.y
	                + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 0 : 1)+' '+sv.borderRadius+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	                + ' L'+E.x+' '+E.y
	                );
	            lineContainer.appendChild(path);
            
	            if(arrowAtEnd) {
	                E.x += arrowLength;
	                lineContainer.appendChild(this.svgArrow(E, 'right', sv.lineWidth, sv.color));
	            }else {
	                S.x -= arrowLength-1;
	                lineContainer.appendChild(this.svgArrow(S, 'left', sv.lineWidth, sv.color));
	            }
	        }
	    }else{
        
	        // General case: S---S1-A                         D----E
	        //                      |                         |
	        //                      B           or            B1
	        //                      |                         |
	        //                      D----E              S-S1--A
        
        
	        if(arrowAtEnd){
	            E.x -= arrowLength; // Make room for the arrow
	        }else{
	            D = { x:S.x+bm.hDistanceFromEnd, y:E.y}; // keep the last kink closer to the arrow
	            S.x += arrowLength;                      // Make room for the arrow
	        }
	        if(addOffset)
	            D.x = Math.round(D.x)+offset;
        
	        A.x = D.x;
	        var B = {x:D.x, y:startBeamIsOnTop ? D.y-sv.borderRadius : D.y+sv.borderRadius};
	        var S1 = {x:A.x-sv.borderRadius, y:A.y};
        
	        var path = this.emptySVGPath(sv.lineWidth, sv.color);
	        path.setAttribute('d',   'M'+S.x+' '+S.y
	            + ' L'+S1.x+' '+S1.y
	            + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 1 : 0)+' '+sv.borderRadius+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	            + ' L'+B.x+' '+B.y
	            + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 0 : 1)+' '+sv.borderRadius+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	            + ' L'+E.x+' '+E.y
	            );
	        lineContainer.appendChild(path);
	        if(arrowAtEnd) {
	            E.x += arrowLength;
	            lineContainer.appendChild(this.svgArrow(E, 'right', sv.lineWidth, sv.color));
	        }else {
	            S.x -= arrowLength-1;
	            lineContainer.appendChild(this.svgArrow(S, 'left', sv.lineWidth, sv.color));
	        }
	    }
	},
    
	    // Creates and adds gantt lines to lineContainer for "start to start" connections.
	addStartToStartLines: function(lineContainer, startBeam, endBeam){
    
	    //  A-S1--S            B-B1--E
	    //  |                  |
	    //  |           or     A2
	    //  A2                 |
	    //  |                  |
	    //  B-B1--E            A-S1--S
    
	    var sv = lineContainer.lineStyles;
	    var bm = lineContainer.beamMeasures;
    
	    var startBeamIsOnTop = bm.start.origin.y < bm.end.origin.y;
	    var addOffset = this.addsOffsetsToMakeLinesCrisp;
	    var offset = addOffset ? (sv.lineWidth % 2 == 0 ? 0.0 : 0.5) : 0.0;
    
	    var S  = { x:bm.start.origin.x, y:bm.start.origin.y+(bm.start.size.height/2.0)};
	    if(addOffset)
	        S.y = Math.round(S.y)+offset;
	    var E  = { x:bm.end.origin.x, y:bm.end.origin.y+(bm.end.size.height/2.0)};
	    if(addOffset)
	        E.y = Math.round(E.y)+offset;
	    var A  = { x:Math.min(S.x-bm.hDistanceFromStart, E.x-bm.hDistanceFromEnd), y:S.y };
	    if(addOffset)
	        A.x = Math.round(A.x)+offset;
	    var B  = { x:A.x, y:E.y };
	    var S1 = { x:A.x+sv.borderRadius, y:A.y };
	    var B1 = { x:B.x+sv.borderRadius, y:B.y };
	    var A2 = { x:A.x, y:B.y+ (startBeamIsOnTop ? -sv.borderRadius : +sv.borderRadius) };
    
	    var arrowLength = this.arrowSizeForLineWidth(sv.lineWidth).width;
	    E.x -= arrowLength;
	    var path = this.emptySVGPath(sv.lineWidth, sv.color);
	    path.setAttribute('d',   'M'+S.x+' '+S.y
	        + ' L'+S1.x+' '+S1.y
	        + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 0 : 1)+' '+(-sv.borderRadius)+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	        + ' L'+A2.x+' '+A2.y
	        + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 0 : 1)+' '+(sv.borderRadius)+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	        + ' L'+E.x+' '+E.y);
	    lineContainer.appendChild(path);
	    E.x += arrowLength;
	    lineContainer.appendChild(this.svgArrow(E, 'right', sv.lineWidth, sv.color));
	},
    
	    // Creates and adds gantt lines to lineContainer for "end to end" connections.
	addEndToEndLines: function(lineContainer, startBeam, endBeam){
    
	    //  S--S1-A            E--B1-B
	    //        |                  |
	    //        |     or          A2
	    //       A2                  |
	    //        |                  |
	    //  E--B1-B            S--S1-A
    
	    var sv = lineContainer.lineStyles;
	    var bm = lineContainer.beamMeasures;
    
	    var startBeamIsOnTop = bm.start.origin.y < bm.end.origin.y;
	    var addOffset = this.addsOffsetsToMakeLinesCrisp;
	    var offset = addOffset ? (sv.lineWidth % 2 == 0 ? 0.0 : 0.5) : 0.0;
    
	    var S  = { x:bm.start.origin.x+bm.start.size.width, y:bm.start.origin.y+(bm.start.size.height/2.0)};
	    if(addOffset)
	        S.y = Math.round(S.y)+offset;
	    var E  = { x:bm.end.origin.x+bm.end.size.width, y:bm.end.origin.y+(bm.end.size.height/2.0)};
	    if(addOffset)
	        E.y = Math.round(E.y)+offset;
	    var A  = { x:Math.max(S.x+bm.hDistanceFromStart, E.x+bm.hDistanceFromEnd), y:S.y };
	    if(addOffset)
	        A.x = Math.round(A.x)+offset;
	    var B  = { x:A.x, y:E.y };
	    var S1 = { x:A.x-sv.borderRadius, y:A.y };
	    var B1 = { x:B.x-sv.borderRadius, y:B.y };
	    var A2 = { x:A.x, y:B.y+ (startBeamIsOnTop ? -sv.borderRadius : sv.borderRadius) };
    
	    var arrowLength = this.arrowSizeForLineWidth(sv.lineWidth).width;
	    E.x += arrowLength;
	    var path = this.emptySVGPath(sv.lineWidth, sv.color);
	    path.setAttribute('d',   'M'+S.x+' '+S.y
	        + ' L'+S1.x+' '+S1.y
	        + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 1 : 0)+' '+sv.borderRadius+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	        + ' L'+A2.x+' '+A2.y
	        + ' a'+sv.borderRadius+' '+sv.borderRadius+' 0 0 '+(startBeamIsOnTop ? 1 : 0)+' '+(-sv.borderRadius)+' '+(startBeamIsOnTop ? sv.borderRadius : -sv.borderRadius)
	        + ' L'+E.x+' '+E.y);
	    lineContainer.appendChild(path);
	    E.x -= arrowLength;
	    lineContainer.appendChild(this.svgArrow(E, 'left', sv.lineWidth, sv.color));
	},

	clipGantt: function(startX, endX){
    
	//    console.log('### clipGantt');
    
	    var clipDataString = '';
    
	    // Clip the gantt
	    var gantt   = this.ganttElement;
	    var clipDiv = gantt.getParent();
	    var clipDivWidth = endX-startX;
	    var clipDivWidthPx = clipDivWidth+'px';
	    clipDiv.setStyles({'overflow-x':'hidden', 'width':clipDivWidthPx, 'min-width':'100%'});
    
	    clipDataString += 'ganttClipDivWidth:'+clipDivWidthPx+',';
    
	    // Set the min-widtn to make the width of the gantt big enough.
	    // The proper formular would be 100% + startx + 'px' but mixed units are not supported by all browsers.
	    // We just set a big absolute value which includes both margins and the offset to the left.
	    // In addition using an absolute value may be faster than a relative value, because the browser has nothing to calculate.
	    var minWidth = (startX+2*2000) + 'px';
	    var startXPx = '-'+startX+'px';
	    var endXPx   = endX+'px';
	    gantt.setStyles({'left':startXPx, 'width':endXPx, 'min-width':minWidth});
    
	    clipDataString += 'ganttMinWidth:'+minWidth+',';
	    clipDataString += 'ganttLeft:'+startXPx+',';
	    clipDataString += 'ganttWidth:'+endXPx+',';
    
	    // Clip the timescale
	    var timeScale = this.timeScaleElement;
	    clipDiv = timeScale.getParent();
	    clipDiv.setStyles({'overflow-x':'hidden', 'width':clipDivWidthPx, 'min-width':'100%'});
	    timeScale.setStyles({'left':startXPx, 'min-width':minWidth});
    
	    // Make the leftmost visible timescale text right aligned
	    clipDataString += 'rightAlignedTierCells:';
	    this.timeScaleElement.getElements('.tier').each(function(tier, tierIndex){
	        tier.getElements('.cell').each(function(cell, cellIndex){
	            var left = parseFloat(cell.getStyle('left'));
	            if(left < startX){
	                if(parseFloat(cell.getStyle('width')) + left >= startX){
	                    cell.setStyles({'text-align':'right', 'padding-right':'5px'});
	                    // Also save the left most tier cell indices into clipDataTextField's value:
	                    clipDataString += cellIndex+'|';
	                }
	            }
	        });
	    });
	    clipDataString = clipDataString.substring(0, clipDataString.length-1);
	    this.clipDataTextField.setProperty('value', clipDataString);
    
	},

    
	updateStartAndEndMargins: function(){
    
	    var gantt = this.ganttElement;
    
	    //gantt.addEvent('click', function(event){console.dir(event)});
    
	    // Left margin calculation
	    function leftLabelTDInRow(row){
	        var barContainers = row.getElements('.barContainer');
	        if(barContainers.length ==0)
	            return null;
        
	        var container = barContainers[0];
	        var result = container.getElement('td:first-child');
	        if(barContainers.length == 2){
	            // We have a secondary bar so we compare the actual positions
	            // The position of a bar, milestone or group is determined by the width of the first td element inside a bar container.
	            var secondaryTD = barContainers[1].getElement('td:first-child');
	            if(secondaryTD.getSize().x < result.getSize().x)
	                result = secondaryTD;
	        }
	        return result;
	    }
    
	    function getBarEndX(container){
	        var TDs = container.getElements('td');
	        return TDs[0].getSize().x + TDs[1].getSize().x;
	    }
    
	    function rightLabelTDInRow(row){
	        var barContainers = row.getElements('.barContainer');
	        if(barContainers.length ==0)
	            return null;
        
	        var container = barContainers[0];
	        var result = container.getElement('td:last-child');
	        if(barContainers.length == 2){
	            var secondaryContainer = barContainers[1];
	            if(getBarEndX(secondaryContainer) > getBarEndX(container))
	                result = secondaryContainer.getElement('td:last-child')
	                }
	        return result;
	    }
    
	    function getBoxesFromRow(row){
	        var b1 = row.getElements('.timePhaseSubrow1 .box');
	        if(!b1 || b1.length == 0)
	            b1 = row.getElements('.timePhaseSubrow1 .hbox1'); // Histogram box
        
	        var b2 = row.getElements('.timePhaseSubrow2 .box');
	        if(!b2 || b2.length == 0)
	            b2 = row.getElements('.timePhaseSubrow2 .hbox1');
        
	        var b3 = row.getElements('.timePhaseSubrow3 .box');
	        if(!b3 || b3.length == 0)
	            b3 = row.getElements('.timePhaseSubrow3 .hbox1');
	        return [(b1 ? b1 : []), (b2 ? b2 : []), (b3 ? b3 : [])];
	    }
    
	    function minXOfTimephaseBoxesInRow(row){
	        var result = null;
        
	        var boxes = getBoxesFromRow(row);
	        var box1 = boxes[0]; box1 = box1.length>0 ? box1[0]: null;
	        var box2 = boxes[1]; box2 = box2.length>0 ? box2[0]: null;
	        var box3 = boxes[2]; box3 = box3.length>0 ? box3[0]: null;
        
	        if(box1)
	            result = parseFloat(box1.getStyle('left'));
	        if(box2){
	            var x = parseFloat(box2.getStyle('left'));
	            result = result ? Math.min(x, result) : x;
	        }
	        if(box3){
	            var x = parseFloat(box3.getStyle('left'));
	            result = result ? Math.min(x, result) : x;
	        }
	        return result ? result : 0.0;
	    }
    
	    function getMaxXOfBox(box){
	        var result = null;
	        if(box) {
	            result  = parseFloat(box.getStyle('left'));
	            result += parseFloat(box.getStyle('width'));
	        }
	        return result;
	    }
    
	    function getLastVisibleBox(boxes){
	        var iLast = boxes.length-1;
	        for(var i=iLast; i>=0; i--)
	        {
	            var box = boxes[i];
	            if(!box.hasClass('invisible'))
	                return box;
	        }
	        return null;
	    }
    
	    function maxXOfTimephaseBoxesInRow(row){
	        var result = null;
        
	        var boxes  = getBoxesFromRow(row);
	        var boxes1 = boxes[0];
	        var boxes2 = boxes[1];
	        var boxes3 = boxes[2];
        
	        if(boxes1 && boxes1.length > 1)
	            result = getMaxXOfBox(getLastVisibleBox(boxes1));
	        if(boxes2 && boxes2.length > 1){
	            var x = getMaxXOfBox(getLastVisibleBox(boxes2));
	            result = result ? Math.max(x, result) : x;
	        }
	        if(boxes3 && boxes3.length > 1){
	            var x = getMaxXOfBox(getLastVisibleBox(boxes3));
	            result = result ? Math.max(x, result) : x;
	        }
	        return result ? result : 0;
	    }
    
	    // Calculate minimum and maximum x values by iterating over all rows:
    
	    var minX = gantt.getProperty('data-earlieststartdateposition').toFloat();
	    var maxX = 0.0;
	    var additionalSpace = 10.0;
    
	    gantt.getElements('.row').each(function(row, index){
        
	        // Calculate the start margin:
	        // We only need to look at the leftmost bar container because this one has a left label.
	        var labelTD = leftLabelTDInRow(row);
	        if(labelTD) {
	            var barX = labelTD.getSize().x;
	            minX = !minX ? barX : Math.min(barX, minX);
	            var leftDIV = labelTD.getElement('.leftLabel');
	            var leftLabelWidth = leftDIV.getSize().x + additionalSpace;    // Add 10 pixel between labels and the left border of the gantt view.
	            var spacer = labelTD.getElement('.spacer');
	            if(spacer)
	                leftLabelWidth += spacer.getSize().x;
	            var leftLabelX = barX - leftLabelWidth;
	            minX = !minX ? leftLabelX : Math.min(leftLabelX, minX);
	        }else{
	            var minXTimephase = minXOfTimephaseBoxesInRow(row) - additionalSpace;
	            if(minXTimephase > 0)
	                minX = !minX ? minXTimephase : Math.min(minX, minXTimephase);
	        }
        
	        // Calculate the end margin:
	        labelTD = rightLabelTDInRow(row);
	        if(labelTD) {
	            barX = getBarEndX(labelTD.getParent('.barContainer'));
	            maxX = Math.max(barX, maxX);
	            var rightDIV = labelTD.getElement('.rightLabel');
	            var rightLabelWidth = rightDIV.getSize().x + additionalSpace;  // Add 10 pixel between labels and the left border of the gantt view.
	            var spacer = labelTD.getElement('.spacer');
	            if(spacer)
	                rightLabelWidth += spacer.getSize().x;
	            var rightLabelX = barX + rightLabelWidth;
	            maxX = Math.max(rightLabelX, maxX);
	        }else{
	            var maxXTimephase = maxXOfTimephaseBoxesInRow(row) + additionalSpace;
	            maxX = Math.max(maxX, maxXTimephase);
	        }
	    });
    
	    this.clipGantt(minX, maxX);
	},


	updateMiddleLabels: function(){
	    this.ganttElement.getElements('.middleLabel[data-short-value]').each(function(div){
	        // Note: Detecting if the text is bigger than the div is not possible using div.scrollWidth > div.clientWidth.
	        //       Because scrollWidth returns the size of the content only if 'overflow' is set to 'scroll'.
	        var textWrapper = div.getElement('div');
        
	        //console.log('textWrapper.width: '+textWrapper.getSize().x+' div.width: '+div.getSize().x);
        
	        if(textWrapper.getSize().x > div.getSize().x){
	            var shortText = div.getProperty('data-short-value');
	            div.innerHTML = shortText;
            
	            // Also replace the text inside the completed part if we have one:
	            var middleLabelCompleted = div.getParent('.bar').getElement('.completed .middleLabel');
	            if(middleLabelCompleted)
	                middleLabelCompleted.innerHTML = shortText;
	        }
	    });
	}
});

// *********** END of MEWGanttLinesController
