/*******************************************************************************
AToMPM - A Tool for Multi-Paradigm Modelling

Copyright (c) 2011 Raphael Mannadiar (raphael.mannadiar@mail.mcgill.ca)
Modified by Conner Hansen (chansen@crimson.ua.edu)

This file is part of AToMPM.

AToMPM is free software: you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later 
version.

AToMPM is distributed in the hope that it will be useful, but WITHOUT ANY 
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along
with AToMPM.  If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/

ConnectionUtils = function(){
	var connectionPathEditingOverlay = {};
	var currentControlPoint = undefined;
	var connectionSource = undefined;
	var connectionPath = undefined;
	
	this.getConnectionSource = function(){
		return connectionSource;
	};
	
	this.getConnectionPath = function(){
		return connectionPath;
	};
	
	/**
	 * "Confirm"s the entire current connection path.
	 * TODO: update this documentation
	 */
	this.addConnectionSegment = function(){
		connectionPath.node.setAttribute('_d',connectionPath.attr('path'));
	};
	
	/**
	 * Adds a new control point to the current path
	 * @param x the x-coordinate
	 * @param y the y-coordinate
	 * @param overlay 
	 */
	this.addControlPoint = function(x,y,overlay) {
		ConnectionUtils.addOrDeleteControlPoint('+',overlay,x,y);
	};
	
	/* Explanation of the Add/Delete algorithm:
		
		addition:
			. clicked overlay corresponds to Lx2,y2
	  		Mx0,y0 Lx1,y1 Lx2,y2 Lx3,y3
				becomes
	  		Mx0,y0 Lx1,y1 Lx2,y2 Lx2,y2 Lx3,y3
	
		deletion:
			. clicked overlay corresponds to Lx2,y2
	  		Mx0,y0 Lx1,y1 Lx2,y2 Lx3,y3
				becomes
	  		Mx0,y0 Lx1,y1 Lx3,y3
	
		after making the described modifications to the corresponding edge's segments
		property, 
		1 the edge is redrawn
		2 a request is sent to the csworker to update the edge's Link's $segments 
		  property 
		3 the connection path editing overlay is refreshed (this will cause newly 
		  added control points to appear, and deleted ones to disappear) 
	 
		NOTE:: the first and last control points can never be deleted */
	/**
	 * Adds or deletes the control point associated with the given overlay.
	 */
	this.addOrDeleteControlPoint = function(op,overlay,x,y){
		if( ! overlay.hasAttribute('__edgeId') )
			return;
		
		var edgeId = overlay.getAttribute('__edgeId'),
			 num = parseInt( overlay.getAttribute('__num') ),
			 offset = parseInt( overlay.getAttribute('__offset') ),
			 segments = __edges[edgeId]['segments'],
			 points = segments.match(/([\d\.]*,[\d\.]*)/g);
	
		if( op == '-' )
			/* delete a control point */
		{
			if( num+offset == 0 || num+offset == points.length-1 )
				return;
			points.splice(num+offset,1);
		}
		else
			/* add a control point */
			points.splice(num+offset,0,x+','+y);
	
		var newpath = 'M'+points.join('L'),
			 edgeIds = utils.keys(connectionPathEditingOverlay),
			 linkuri = __edgeId2linkuri(edgeId),
			 changes = {};
		
		changes[edgeId] = newpath;
		__redrawEdge(edgeId,newpath);
		DataUtils.updatecs(
			linkuri, 
			{'$segments':utils.mergeDicts([__linkuri2segments(linkuri),changes])});
		ConnectionUtils.hideConnectionPathEditingOverlay();
		ConnectionUtils.showConnectionPathEditingOverlay(edgeIds);	
	};
	
	/**
	 * Removes the current control point
	 * @param overlay - the overlay to be used to identify the control point
	 */
	this.deleteControlPoint = function(overlay)	{
		ConnectionUtils.addOrDeleteControlPoint('-',overlay);
	};
	
	/**
	 * "Unconfirm" the last segment of the connection path (ie
	 * remove it). Do nothing if all segments have been "confirmed"
	 */
	this.deleteConnectionSegment = function() {
		var d = String(connectionPath.attr('path')),
		matches = d.match(/(M.*,.*)L(.*),(.*)/),
		_d = connectionPath.node.getAttribute('_d'),
		_matches = _d.match(/(M.*,.*)L.*,.*/);

		if( ! _matches )
			; // do nothing
		else if( matches )
		{
			var x = matches[2], y = matches[3];
			connectionPath.node.setAttribute('_d',_matches[1]);
			ConnectionUtils.updateConnectionSegment(x,y);
		}
	};
	
	/* 
	NOTE:: connectionSource is used to remember the uri of the icon at the 
			 start of the path
	NOTE:: _d is used ro remember the 'confirmed' portions of the path
	NOTE:: the call to connectionPath.toBack() causes mouse events that occur
			 on top of icons to be captured by those (as opposed to by the in-
			 progress connection path which would capture them otherwise) 
	 */
	/**
	 * Initializes a Raphael Path starting at (x, y) and that reports the
	 * mouseup event as if it were the canvas
	 */
	this.initConnectionPath = function(x,y,target){
		if( connectionPath != undefined )
			return;

		connectionSource = __vobj2uri(target);
		connectionPath = __canvas.path('M'+x+','+y);
		connectionPath.node.setAttribute('_d','M'+x+','+y);
		connectionPath.toBack();		
		connectionPath.node.onmouseup = function(event) {
			if( event.button == 0 )
				BehaviorManager.handleUserEvent(__EVENT_LEFT_RELEASE_CANVAS,event);
			else if( event.button == 1 )
				BehaviorManager.handleUserEvent(__EVENT_MIDDLE_RELEASE_CANVAS,event);
			else if( event.button == 2 )
				BehaviorManager.handleUserEvent(__EVENT_RIGHT_RELEASE_CANVAS,event);
		};
	};
	
	/**
	 * Saves the Raphael element associated with the specific overlay as the current
	 * control point. 
	 * 
	 * This provides a more robust defense against moving the mouse so quickly that it
	 * exits the overlay we're dragging.
	 */
	this.initControlPointTranslation = function(overlay){
		if( overlay.hasAttribute('__edgeId') )
			/* set currentControlPoint to normal overlay */								
		{
			var edgeId = overlay.getAttribute('__edgeId'),
				 num	  = overlay.getAttribute('__num');
			currentControlPoint = connectionPathEditingOverlay[edgeId][num];
		}
		else
			/* set currentControlPoint to central overlay */								
		{
			var linkuri = overlay.getAttribute('__linkuri');
			currentControlPoint = connectionPathEditingOverlay[linkuri][0];
		}
	};
	
	/**
	 * Hide and delete the connection path
	 */
	this.hideConnectionPath = function(){
		connectionPath.remove();
		connectionPath = undefined;
		connectionSource = undefined;
	};
	
	/**
	 * Hides the current connection path overlay
	 */
	this.hideConnectionPathEditingOverlay = function(){
		for( var _ in connectionPathEditingOverlay )
			connectionPathEditingOverlay[_].forEach(
					function(overlay)
					{
						overlay.remove();
					});
		
		connectionPathEditingOverlay = {};
		currentControlPoint = undefined;
	};
	
	/**
	 * Moves the control point and its overlay to the specified coordinates
	 */
	this.previewControlPointTranslation = function(x,y){
		var _x = parseInt( currentControlPoint.node.getAttribute('_x') ),
			 _y = parseInt( currentControlPoint.node.getAttribute('_y') );
		currentControlPoint.translate(x-_x,y-_y);
		currentControlPoint.node.setAttribute('_x',x);
		currentControlPoint.node.setAttribute('_y',y);
		ConnectionUtils.updateConnectionPath(true);
	};
	
	/**
	 * Show the connection path editing overlay. This shows draggable circles
	 * above every control point along the selected edges. 
	 */
	this.showConnectionPathEditingOverlay = function(_edgeIds){
		var edgeIds 	 = 
				(_edgeIds ? _edgeIds : __selection['items']).
					filter( function(it)	{return it in __edges;} ),
			 onmousedown = 
				function(event)
				{
					if( event.button == 0 )
						BehaviorManager.handleUserEvent(__EVENT_LEFT_PRESS_CTRL_POINT,event);
				},
			 onmouseup = 
			 	function(event)
				{
					if( event.button == 0 )
						BehaviorManager.handleUserEvent(__EVENT_LEFT_RELEASE_CTRL_POINT,event);
					else if( event.button == 1 )
						BehaviorManager.handleUserEvent(__EVENT_MIDDLE_RELEASE_CTRL_POINT,event);
					else if( event.button == 2 )
						BehaviorManager.handleUserEvent(__EVENT_RIGHT_RELEASE_CTRL_POINT,event);
				};
	
		edgeIds.forEach(
			function(edgeId)
			{
				var points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g),
					linkuri = __edgeId2linkuri(edgeId),
					edgeToLink = edgeId.match(linkuri+'$');
	
				/* setup normal overlay */								
				connectionPathEditingOverlay[edgeId] = [];
				(edgeToLink ? 
				 	points.slice(0,points.length-1) :
					points.slice(1)).forEach(
					function(p)
					{
						var xy		= p.split(','),
							 x 		= xy[0],
							 y 		= xy[1],
							 overlay	= __canvas.circle(x,y,5);
						overlay.node.setAttribute('class','ctrl_point_overlay');
						overlay.node.setAttribute('__edgeId',edgeId);
						overlay.node.setAttribute('__offset',(edgeToLink ? 0 : 1));
						overlay.node.setAttribute('__num',
								connectionPathEditingOverlay[edgeId].length);
						overlay.node.setAttribute('_x',x);
						overlay.node.setAttribute('_y',y);
						overlay.node.onmouseup = onmouseup;
						overlay.node.onmousedown = onmousedown;
						connectionPathEditingOverlay[edgeId].push(overlay);
					});
	
				/* enhance start/end */							
				if( edgeToLink )
					utils.head(connectionPathEditingOverlay[edgeId]).node.
						setAttribute('__start', __edges[edgeId]['start']);
				else
					utils.tail(connectionPathEditingOverlay[edgeId]).node.
						setAttribute('__end', __edges[edgeId]['end']);
	
				/* setup central overlay */	
				var edgeListAttr = (edgeToLink ? '__edgesTo' : '__edgesFrom');
				if( ! (linkuri in connectionPathEditingOverlay) )
				{
					var xy			= (edgeToLink ?
				__edges[edgeId]['segments'].match(/.*L(.*)/) :
				__edges[edgeId]['segments'].match(/M([\d\.]*,[\d\.]*)/))[1].split(','),
						 x 			= xy[0],
						 y 			= xy[1],
						 overlay	= __canvas.circle(x,y,8);
					overlay.node.setAttribute('class','ctrl_point_center_overlay');
					overlay.node.setAttribute('_x',x);
					overlay.node.setAttribute('_y',y);
					overlay.node.setAttribute('_x0',x);
					overlay.node.setAttribute('_y0',y);
					overlay.node.setAttribute('__linkuri',linkuri);
					overlay.node.setAttribute('__edgesTo',utils.jsons([]));
					overlay.node.setAttribute('__edgesFrom',utils.jsons([]));
					overlay.node.onmouseup = onmouseup;
					overlay.node.onmousedown = onmousedown;
					connectionPathEditingOverlay[linkuri] = [overlay];
				}
	
				var centerOverlay	= connectionPathEditingOverlay[linkuri][0],
					 edgeList 		= utils.jsonp(centerOverlay.node.getAttribute(edgeListAttr));
				edgeList.push(edgeId);
				centerOverlay.node.setAttribute(edgeListAttr,utils.jsons(edgeList));
			});
	};
	
	/**
	 * Snaps the current segment to the x or y axis depending on its proximity
	 * to both axes
	 */
	this.snapConnectionSegment = function(x,y){
		var _d		 = connectionPath.node.getAttribute('_d'),
			 _matches = _d.match(/.*[L|M](.*),(.*)/),
			 _x 		 = parseInt( _matches[1] ),
			 _y 		 = parseInt( _matches[2] ),
			 d		 	 = String(connectionPath.attr('path')),
			 matches  = d.match(/.*[L|M](.*),(.*)/),
			 x 		 = parseInt( matches[1] ),
			 y 		 = parseInt( matches[2] );
		
		if( Math.abs(x-_x) > Math.abs(y-_y) )
			y = _y;
		else
			x = _x;
		ConnectionUtils.updateConnectionSegment(x,y);
	};
	
	/**
	 * Snap the current control point, if any
	 */
	this.snapControlPoint = function(){
		if( currentControlPoint == undefined )
			return;
	
		var cpn = currentControlPoint.node,
			 _x  = cpn.getAttribute('_x'),
			 _y  = cpn.getAttribute('_y');
	
		if( cpn.hasAttribute('__edgeId') )
			/* snapping normal overlay */				
		{
			var edgeId = cpn.getAttribute('__edgeId'),
				 num	  = parseInt(cpn.getAttribute('__num')),
				 offset = parseInt(cpn.getAttribute('__offset')),
				 points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g),
				 prevXY = points[num+offset-1];
			if( num+offset == 0 || num+offset == points.length-1 )
				/* don't snap end points */
				return;
		}
		else
			/* snapping central overlay */				
			var edgeId = utils.jsonp(cpn.getAttribute('__edgesTo'))[0],
				 points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g),
				 prevXY = points[points.length-2];
	
		prevXY = prevXY.split(',');
		if( Math.abs(prevXY[0]-_x) > Math.abs(prevXY[1]-_y) )
			_y = prevXY[1];
		else
			_x = prevXY[0];
		ConnectionUtils.previewControlPointTranslation(_x,_y);
		ConnectionUtils.updateConnectionPath();
	};
	
	/* 	NOTE:: when 'local' is false/omitted, edge and center-piece alterations are
		not merely displayed, but also persisted to the csworker 
	*/
	/**
	 * Alters edges and/or center-pieces to ensure they follow the changes
	 * effected to their overlays by ConnectionUtils.previewControlPointTranslation()
	 * and ConnectionUtils.snapConnectionSegment(). This function redraws edges and/or
	 * moves center pieces
	 */
	this.updateConnectionPath = function(local){
		var cpn = currentControlPoint.node,
			 _x  = cpn.getAttribute('_x'),
			 _y  = cpn.getAttribute('_y');
	
		function updatedCenterPiecePosition()
		{
			var linkuri = cpn.getAttribute('__linkuri'),
				 x0 		= parseInt( cpn.getAttribute('_x0') ),
				 y0 		= parseInt( cpn.getAttribute('_y0') ),
				 icon		= __icons[linkuri]['icon'];
			cpn.setAttribute('_x0',_x);
			cpn.setAttribute('_y0',_y);
			return [(_x-x0) + parseFloat(icon.getAttr('__x')), 
					  (_y-y0) + parseFloat(icon.getAttr('__y'))];
		}
	
		function updateEdgeExtremity(edgeId,start)
		{
			var matches	= __edges[edgeId]['segments'].
					 				match(/(M[\d\.]*,[\d\.]*)(.*)(L.*)/),
				 newpath	= (start ?
						 			'M'+_x+','+_y+matches[2]+matches[3] :
									matches[1]+matches[2]+'L'+_x+','+_y);
			__redrawEdge(edgeId,newpath);
			return newpath;
		}
	
		function updateInnerEdge(edgeId,idx)
		{
			var points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g);
			points.splice(idx,1,_x+','+_y);
			var newpath = 'M'+points.join('L');
			__redrawEdge(edgeId,newpath);
			return newpath;
		}
	
	
		if( cpn.hasAttribute('__edgeId') )
			/* dragging normal overlay */		
		{
			var edgeId 	= cpn.getAttribute('__edgeId'),
				 num		= cpn.getAttribute('__num'),
				 offset	= cpn.getAttribute('__offset'),
				 linkuri = __edgeId2linkuri(edgeId),
				 changes	= {};
			changes[edgeId] = updateInnerEdge(edgeId,parseInt(num)+parseInt(offset));
		}
		else	
			/* dragging central overlay */
		{
			var linkuri = cpn.getAttribute('__linkuri'),
				 changes = {};
			utils.jsonp( cpn.getAttribute('__edgesTo') ).forEach(
					function(edgeId)
					{
						changes[edgeId] = updateEdgeExtremity(edgeId,false);
					});
			utils.jsonp( cpn.getAttribute('__edgesFrom') ).forEach(
					function(edgeId)
					{
						changes[edgeId] = updateEdgeExtremity(edgeId,true);
					});
		}
		
		if( ! local )
			DataUtils.updatecs(
				linkuri,
				utils.mergeDicts([
					{'$segments':utils.mergeDicts(
												[__linkuri2segments(linkuri),changes])},
					(cpn.hasAttribute('__linkuri') ?
						 {'position' :updatedCenterPiecePosition()} : {})]));
	};
	
	/**
	 * Redraws the current segment such that its end is at (x, y)
	 */
	this.updateConnectionSegment = function(x,y){
		connectionPath.attr(
			'path',
			connectionPath.node.getAttribute('_d')+'L'+x+','+y);
	};

	return this;
}();