/*
 * PiAnoS (Picture Annotation System), A fingerprint examiner training tool
 * Copyright (C) 2011-2021 University of Lausanne, Switzerland
 * https://ips-labs.unil.ch/pianos
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * This program 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 Affero General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */

/**
 * @constructor
 */
function Pianos4ToolEdit(params)
{
	// ------------------------------
	// Constructor
	// ------------------------------
	
	params = params || {};
	if (!params.lineColor) params.lineColor = '#dddd00';
	if (!params.fillColor) params.fillColor = '#101000';
	if (typeof(params.v3) == 'undefined') params.v3 = false;
	if (!params.hotkey) params.hotkey = 'ctrl';
	
	// ------------------------------
	// Public methods
	// ------------------------------
	
	this.getName = function()
	{
		return 'edit';
	}
	
	this.getLabel = function()
	{
		return 'Edit';
	}
	
	this.getIcon = function()
	{
		return 'icons/select.png';
	}
	
	this.bind = function(aView)
	{
		assert(!view);
		view = aView;
		// if (!(view in gizmos))
		// gizmos[view] = {};
	}
	
	this.unbind = function()
	{
		assert(view);
		view = null;
	}
	
	this.activate = function()
	{
		log('edit tool activate');
	}
	
	this.deactivate = function()
	{
		log('edit tool de-activate');
	}
	
	this.onMouseDown = function(pos, target, event)
	{
		if (popup)
			return false;
		
		var posnotrotated = pos;
		var rotationData = rotationView[ view.getType() ];
        pos = rotateAround( pos, rotationData, false );
		
		// log(event);
		var picked = view.pickByNode(target);
		if (!picked)
		{
			assert(!dragSel);
			dragSel = {
				from: posnotrotated,
				to: posnotrotated,
				quad: view.getSVG().append('svg:rect')
						.attr('x', posnotrotated[0])
						.attr('y', posnotrotated[1])
						.attr('width', 0)
						.attr('height', 0)
						.style('stroke', params.lineColor)
						.style('stroke-width', 1.0/view.getZoomFactor())
						.style('fill', params.lineColor)
						.style('fill-opacity', 0.3)
			}
		}
		else
		{
			assert(!manipulated);
			
			var movingOnFirstClick = false;
			if (!view.isSelected(picked.annotation))
			{
				view.selectAnnotation(picked.annotation, !event.ctrlKey);
				// Since we will not "see" the gizmo in the DOM (as the picking
                // already occured before
				// the selection, no action will be triggered. This flag works
                // around this by forcing
				// a move, which is quite natural and allows one to
                // select-and-drag an object that was
				// initially not selected. Note that minutiae cannot be rotated
                // and ridge/area points
				// moved this way - but again, the user will find the behaviour
                // quite sensible.
				movingOnFirstClick = true;
			}
				
			manipulated = { obj: picked, pos: pos, action: null };

			if (picked.type == 'minutia')
			{
				if (svgHasClass(picked.originalNode, '_gizmo') || movingOnFirstClick)
				{
					log('starting move action');
					manipulated.action = 'move';
					manipulated.transform = getSVGTransform(picked.node, true);
					manipulated.origPos = [manipulated.transform.matrix.e, manipulated.transform.matrix.f];
				}
				else if (svgHasClass(picked.originalNode, '_rot'))
				{
					log('starting rot action');
					manipulated.action = 'rotate';
					manipulated.transform = getSVGTransform(picked.node, false);
				}
			}
			else if (picked.type == 'ridge' || picked.type == 'area')
			{
				if (svgHasClass(picked.originalNode, '_handle'))
				{
					// Moving a ridge/area handle
					manipulated.action = 'move';
					var gizmo = d3.select(picked.node).select('path._gizmo').node();
					manipulated.pointID = parseInt(picked.originalNode.id.split('_')[2]);
					assert(manipulated.pointID >= 0 && manipulated.pointID < gizmo.pathSegList.numberOfItems);
					manipulated.pathPoint = gizmo.pathSegList.getItem(manipulated.pointID);
					manipulated.handle = d3.select(picked.originalNode);
					manipulated.origPos = [manipulated.pathPoint.x, manipulated.pathPoint.y];
					log(manipulated);
				}
				else
				{
					// Moving the ridge/area itself (most likely to make sense
                    // only for areas but who knows)
					log(picked);
					manipulated.action = 'move';
					// Apply a transform to the node while we drag it - we'll
                    // remove it on mouse release
					d3.select(picked.node).attr('transform','translate(0,0)');
					manipulated.transform = getSVGTransform(picked.node, true);
					manipulated.origPos = [manipulated.transform.matrix.e, manipulated.transform.matrix.f];
				}
			}
		}
	}
	
	this.onMouseMove = function(pos, target)
	{
		if (popup)
			return false;
		
		var posnotrotated = pos;
		var rotationData = rotationView[ view.getType() ];
        pos = rotateAround( pos, rotationData, false );
		
		if (dragSel)
		{
			dragSel.to = posnotrotated;
			var p = $.extend(true,[],dragSel.from);
			var size = [posnotrotated[0]-p[0], posnotrotated[1]-p[1]];
			for (var i=0;i<2;++i) {
				if (size[i] < 0.0) {
					p[i] += size[i];
					size[i] = Math.abs(size[i]);
				}
			} 
			dragSel.quad.attr('x', p[0]).attr('y', p[1]).attr('width', size[0]).attr('height', size[1]);
			dragSel.tl = p;
			dragSel.br = [p[0]+size[0],p[1]+size[1]];
		}
		else if (manipulated && manipulated.action)
		{
			if (manipulated.action == 'move')
			{
				manipulated.offset = [pos[0]-manipulated.pos[0], pos[1]-manipulated.pos[1]];
				var newPos = [manipulated.origPos[0]+manipulated.offset[0], manipulated.origPos[1]+manipulated.offset[1]];
				
				if ('transform' in manipulated)
				{
					manipulated.transform.setTranslate(newPos[0], newPos[1]);
				}
				else if ('pathPoint' in manipulated)
				{
					manipulated.pathPoint.x = newPos[0];
					manipulated.pathPoint.y = newPos[1];
					manipulated.handle.attr('cx', newPos[0])
										.attr('cy', newPos[1]);
				}
				else
				{
					assert(false);
				}
			}
			else if (manipulated.action == 'rotate')
			{
				var offset = [pos[0]-manipulated.obj.annotation.pos[0], pos[1]-manipulated.obj.annotation.pos[1]];
				var l = Math.sqrt(offset[0]*offset[0]+offset[1]*offset[1]);
				var o = [offset[0]/l, offset[1]/l];
				var acos = Math.acos(o[0]);
				var anglerad = Math.asin(o[1]) < 0 ? -acos : acos;
				var angle = anglerad/Math.PI*180.0;
				manipulated.transform.setRotate(angle, 0, 0);
				manipulated.angle = angle;
			}
		}
	}
	
	this.onMouseUp = function(pos, target, event)
	{
		if (!popup)
		{
			if (dragSel)
			{
				dragSel.quad.remove();
				
				view.selectAnnotation(null, true);

				if ('tl' in dragSel)
				{
				    var rotationData = rotationView[ view.getType() ];
				    
					var sel = [];
					var minutiae = view.getData('minutiae').minutiae;
					for (id in minutiae)
					{
					    var min = minutiae[ id ];
	                    var minrotatedpos = rotateAround( min.pos, rotationData, true );
	                    
	                    if( minrotatedpos[ 0 ] >= dragSel.tl[ 0 ] && minrotatedpos[ 0 ] <= dragSel.br[ 0 ] &&
                            minrotatedpos[ 1 ] >= dragSel.tl[ 1 ] && minrotatedpos[ 1 ] <= dragSel.br[ 1 ] )
                            sel.push( min );
					}
					view.selectAnnotation(sel, false);
				}
				
				dragSel = null;
			}
			else if (manipulated)
			{
				if (event.button == 2)
				{
					var picked = view.pickByNode(target);
					if (picked && picked.type == manipulated.obj.type && picked.id == manipulated.obj.id)
					{
						log('Showing context menu for '+picked.type+' '+picked.id);
						showPopup(pos);
						return false;
					}
				}
				else if (manipulated.action)
				{
					if (manipulated.action == 'move' && 'offset' in manipulated)
					{
						var a = manipulated.obj.annotation;
						var o = manipulated.offset;
						
						var mods = {};
						var label = null;
						if (manipulated.obj.type == 'minutia')
						{
							mods = { pos: [a.pos[0]+o[0], a.pos[1]+o[1]] };
							label = 'Move minutia';
						}
						else if (manipulated.obj.type == 'ridge' || manipulated.obj.type == 'area')
						{
							// We make a deep copy of the vector of points so
                            // that undo/redo acts on different arrays
							var newPoints = $.extend(true, [], a.points);
							if ('pointID' in manipulated)
							{
								// Moving a single point
								label = 'Move '+manipulated.obj.type+' point';
								assert(manipulated.pointID >= 0 && manipulated.pointID < newPoints.length);
								newPoints[manipulated.pointID][0] += o[0];
								newPoints[manipulated.pointID][1] += o[1];
							}
							else
							{
								// Moving the complete ridge/area is akin to
                                // moving all the points identically
								label = 'Move '+manipulated.obj.type;
								for (var i=0;i<newPoints.length;++i)
								{
									newPoints[i][0] += o[0];
									newPoints[i][1] += o[1];
								}
								if ('holes' in a)
								{
									var newHoles = $.extend(true, [], a.holes);
									for (var h in newHoles)
									{
										var hole = newHoles[h];
										for (var i=0;i<hole.length;++i)
										{
											hole[i][0] += o[0];
											hole[i][1] += o[1];
										}
									}
									mods.holes = newHoles;
								}
								// Don't forget to remove the transform
                                // attribute
								d3.select(manipulated.obj.node).attr('transform',null);
							}
							mods.points = newPoints;
						}
						
						var action = new Pianos4Action_ModifyAnnotation(view, a, mods);
						if (label)
							action.setLabel(label);
						pianos.exec(action);
					}
					else if (manipulated.action == 'rotate' && manipulated.angle)
					{
						assert(manipulated.obj.type == 'minutia');
						var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, { angle: manipulated.angle })
						action.setLabel('Rotate minutia');
						pianos.exec(action);
					}
					else if (manipulated.action == 'move' && !('offset' in manipulated) && (manipulated.obj.type == 'ridge' || manipulated.obj.type == 'area') && !('pointID' in manipulated))
					{
						// Corner case to remove the transform attribute for
                        // no-op ridge/area dragging
						d3.select(manipulated.obj.node).attr('transform',null);
					}
				}

				manipulated = null;
			}
			return true;
		}
		else
		{
			assert(manipulated && !dragSel);
	
			hidePopup();
			
			if (popup.choice)
			{
				log('Popup choice '+popup.choice);
				
				switch (popup.choice)
				{
				case 'delete':
					view.deleteSelection();
					break;
					
				case 'rempair': 
					(function() {
						assert('pairing' in manipulated.obj.annotation);
						var markMin = null;
						var printMin = null;
						if (view.isMark())
							markMin = manipulated.obj.annotation;
						else
							printMin = manipulated.obj.annotation;
						var action = new Pianos4Action_PairMinutiae(markMin, printMin, null);
						pianos.exec(action);					
						pianos.onSelectionChanged(view);
					})(); 
					break;
				
				case 'group':
					(function() {
						var sel = view.getSelection();
						var annots = [];
						for (var i in sel)
							annots.push(view.getAnnotationById(i)[1]);
						var action = new Pianos4Action_GroupAnnotations(view, annots, 1);
						pianos.exec(action);					
					})(); 
					break;
					
				case 'bifurcation':
				case 'ridge_ending':
				case 'center':
				case 'unknown':
				case 'unknown2':
				case 'difference':
					var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, {type: popup.choice});
					action.setLabel('Change minutia type');
					pianos.exec(action);
					break;
					
				case 'addpoint':
					log(manipulated);
					var segID = getSegmentAt(manipulated.obj.annotation, manipulated.pos);
					segID = Math.max(segID, 0);
					log('Adding point in segment '+segID);
					var points = manipulated.obj.annotation.points;
					var newPoints = points.slice(0, segID+1).concat([manipulated.pos]).concat(points.slice(segID+1));
					var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, { points: newPoints });
					action.setLabel('Add point to '+manipulated.obj.type);
					pianos.exec(action);
					break;
					
				case 'delpoint':
					var points = manipulated.obj.annotation.points;
					assert('pointID' in manipulated && manipulated.pointID >= 0 && manipulated.pointID < points.length);
					var newPoints = points.slice(0, manipulated.pointID).concat(points.slice(manipulated.pointID+1));
					var action = new Pianos4Action_ModifyAnnotation(view, manipulated.obj.annotation, { points: newPoints });
					action.setLabel('Remove '+manipulated.obj.type+' point');
					pianos.exec(action);
					break;
				}
				
			}
			
			popup = null;
			manipulated = null;
			
			if (event.button == 2)
				pianos.skipNextContextMenu();

			return true;
		}
	}
	
	this.cancel = function()
	{
		// if (line)
			// line.remove();
		// line = null;
		if (popup)
			hidePopup();
		popup = null;
	}

	this.getOptionsPanel = function()
	{
		if (!options)
		{
			var panel = $('<div class="pianos4_edit_options" />');
			
			panel.append($('<div class="_hint" />').text('Use ').append($('<strong />').text('Ctrl+Click')).append(' to select multiple annotations'));
			
			options = panel;
		}
		
		return options;
	}
	
	this.getHelp = function()
	{
		return Pianos4Help.compile([
			'Use the edit tool to select and modify annotations.',
			'<<mouse:left>>: select an annotation',
			'<<key:ctrl>><<mouse:left>>: add to the selection',
			'<<key:shift>>: (hold) temporarily switch to the edit tool',
			'<<key:E>>: (shortcut) switch to the edit tool',
		]);
	}
	
	// ------------------------------
	// Private variables
	// ------------------------------
	
	var self = this;
	var view = null;
	var manipulated = null;
	var dragSel = null;
	var popup = null;
	var options = null;
	
	// ------------------------------
	// Private functions
	// ------------------------------
	
	var showPopup = function(pos)
	{
		assert(!popup && manipulated);
		
		var docPos = view.getAbsPos(pos);
		popup = {
			overlay: $('<div class="ui-widget-overlay" style="width:'+$(document).width()+'px;height:'+$(document).height()+'px;z-index:1000;" />'),
			menu: $('<div class="ui-widget ui-widget-content pianos4_canvas_popup" style="z-index:1001;"></div>'),
			choice: null,
		};

		var items = {
			'Annotation': {
				'delete': 'Delete',
				'rempair': 'Remove pairing',
				'group': 'Group selected',
			},
		};
		var disabled = {};
		
		if (manipulated.obj.type == 'minutia')
		{
			if (params.v3)
			{
				items['Minutia type'] = {
					'bifurcation': 'Bifurcation',
					'ridge_ending': 'Ridge ending',
					'unknown': 'Unknown',
					'center': 'Center',
				};
			}
			else
			{
				items['Minutia type'] = {
					'bifurcation': 'Bifurcation',
					'ridge_ending': 'Ridge ending',
					'unknown': 'Type unknown',
					'unknown2': 'Position unknown',
					'difference': 'Difference',
					'center': 'Center',
				};
			}
		}
		else if (manipulated.obj.type == 'ridge' || manipulated.obj.type == 'area')
		{
			items['Edit shape'] = {
				'addpoint': 'Add point here',
				'delpoint': 'Remove this point',
			};
			
			if ('pointID' in manipulated)
			{
				disabled.addpoint = true;
				var minPoints = manipulated.obj.type == 'ridge' ? 2 : 3;
				if (manipulated.obj.annotation.points.length <= minPoints)
					disabled.delpoint = true;
			}
			else
				disabled.delpoint = true;
				
		}
		if ( view.isMark() & ! params.markCanHaveCenter )
			disabled.center = true;
		
		if (!('pairing' in manipulated.obj.annotation))
			disabled.rempair = true;
		
		if (view.getSelectionSize() <= 1)
			disabled.group = true;
			
		var ul = $('<ul />');
		for (var section in items)
		{
			ul.append($('<li />').text(section).addClass('ui-widget-header'));
			
			for (var title in items[section])
			{
				var item = items[section][title];
				var li = $('<li />').text(item)
					.attr('title',title);
				if (title in disabled)
				{
					li.addClass('ui-state-disabled');
				}
				else
				{
					li
						.addClass('ui-state-default')
						.bind('mouseenter', function() { $(this).removeClass('ui-state-default').addClass('ui-state-active'); assert(!popup.choice); popup.choice = this.title; })
						.bind('mouseleave', function() { $(this).removeClass('ui-state-active').addClass('ui-state-default'); assert(popup.choice); popup.choice = null; })
					;
				}
				ul.append(li);
			}
		}
		popup.menu.append(ul);
		
		popup.menu.css('visibility','none');
		$('body').append(popup.overlay).append(popup.menu);

		var docSize = [$(window).width(), $(window).height()];
		var menuPos = [
			Math.max(0, Math.min(docSize[0]-popup.menu.outerWidth(), docPos[0])),
			Math.max(0, Math.min(docSize[1]-popup.menu.outerHeight(), docPos[1]))
		];
		popup.menu.css('left', menuPos[0]+'px')
					.css('top', menuPos[1]+'px')
					.removeCSS('visibility');
					
		pianos.enterModal();
	}
	
	var hidePopup = function()
	{
		popup.overlay.remove();
		popup.menu.remove();
		pianos.leaveModal();
	}
	
	var getSVGTransform = function(node, isTranslate)
	{
		var type = isTranslate ? SVGTransform.SVG_TRANSFORM_TRANSLATE : SVGTransform.SVG_TRANSFORM_ROTATE;
		for (var i=0;i<node.transform.baseVal.numberOfItems;++i)
		{
			var tr = node.transform.baseVal.getItem(i);
			if (tr.type == type)
				return tr;
		}
		assert(false);
	}
	
	var getSegmentAt = function(annotation, point)
	{
		var compat = [];

		for (var i=1;i<annotation.points.length;++i)
		{
			var a = annotation.points[i-1];
			var b = annotation.points[i];
			
			var u = [b[0]-a[0], b[1]-a[1]];
			var lu = Math.sqrt(u[0]*u[0]+u[1]*u[1]);
			var un = [u[0]/lu, u[1]/lu];
			
			var v = [point[0]-a[0], point[1]-a[1]];
			var lv = Math.sqrt(v[0]*v[0]+v[1]*v[1]);
			var vn = [v[0]/lv, v[1]/lv];
			
			var cost = un[0]*vn[0]+un[1]*vn[1];
			
			var proj = cost*lv;
			
			if (proj >= 0 && proj < lu)
			{
				var proj2 = Math.sqrt(lv*lv-proj*proj);
				compat.push({i:i-1,p:proj2});
			}
		}
		
		if (compat.length)
		{
			compat.sort(function(a,b) { return a.p-b.p; });
			return compat[0].i;
		}
		
		return -1;
	}		


}
