/*	Copyright (c) 2016 Jean-Marc VIGLINO, 
	released under the CeCILL-B license (French BSD license)
	(http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
/** Interaction to snap to guidelines
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @param {olx.interaction.SnapGuidesOptions} 
 *	- pixelTolerance {number | undefined} distance (in px) to snap to a guideline, default 10 px
 *  - enableInitialGuides {bool | undefined} whether to draw initial guidelines based on the maps orientation, default false.
 *	- style {ol.style.Style | Array<ol.style.Style> | undefined} Style for the sektch features.
 */
ol.interaction.SnapGuides = function(options) {
	if (!options) options = {};
	// Intersect 2 guides
	function getIntersectionPoint (d1, d2)
	{	var d1x = d1[1][0] - d1[0][0];
		var d1y = d1[1][1] - d1[0][1];
		var d2x = d2[1][0] - d2[0][0];
		var d2y = d2[1][1] - d2[0][1];
		var det = d1x * d2y - d1y * d2x;
		if (det != 0)
		{	var k = (d1x * d1[0][1] - d1x * d2[0][1] - d1y * d1[0][0] + d1y * d2[0][0]) / det;
			return [d2[0][0] + k*d2x, d2[0][1] + k*d2y];
		}
		else return false;
	}
	function dist2D (p1,p2)
	{	var dx = p1[0]-p2[0];
		var dy = p1[1]-p2[1];
		return Math.sqrt(dx*dx+dy*dy);
	}
	// Snap distance (in px)
	this.snapDistance_ = options.pixelTolerance || 10;
	this.enableInitialGuides_ = options.enableInitialGuides || false;
	// Default style
	var sketchStyle =
	[	new ol.style.Style({
			stroke: new ol.style.Stroke(
			{	color: '#ffcc33',
				lineDash: [8,5],
				width: 1.25
			})
		})
	];
	// Custom style
	if (options.style) sketchStyle = options.style instanceof Array ? options.style : [options.style];
	// Create a new overlay for the sketch
	this.overlaySource_ = new ol.source.Vector(
		{	features: new ol.Collection(),
			useSpatialIndex: false
		});
	this.overlayLayer_ = new ol.layer.Vector({
		// render the snap guides as an image to improve performance on rerenders
		renderMode: 'image',
		source: this.overlaySource_,
			style: function() {
				return sketchStyle;
			},
			name:'Snap overlay',
			displayInLayerSwitcher: false
		});
	// Use snap interaction
	ol.interaction.Interaction.call(this, {
		handleEvent: function(e) {
			if (this.getActive()) {
				var features = this.overlaySource_.getFeatures();
				var prev = null;
				var p = null;
				var res = e.frameState.viewState.resolution;
				for (var i=0, f; f = features[i]; i++) {
					var c = f.getGeometry().getClosestPoint(e.coordinate);
					if ( dist2D(c, e.coordinate) / res < this.snapDistance_) {
						// Intersection on 2 lines
						if (prev) {
							var c2 = getIntersectionPoint(prev.getGeometry().getCoordinates(),  f.getGeometry().getCoordinates());
							if (c2) {
								if (dist2D(c2, e.coordinate) / res < this.snapDistance_) {
									p = c2;
								}
							}
						} else {
							p = c;
						}
						prev = f;
					}
				}
				if (p) e.coordinate = p;
			}
			return true;
		}
	});
};
ol.inherits(ol.interaction.SnapGuides, ol.interaction.Interaction);
/**
 * Remove the interaction from its current map, if any,  and attach it to a new
 * map, if any. Pass `null` to just remove the interaction from the current map.
 * @param {ol.Map} map Map.
 * @api stable
 */
ol.interaction.SnapGuides.prototype.setMap = function(map) {
	if (this.getMap()) this.getMap().removeLayer(this.overlayLayer_);
	ol.interaction.Interaction.prototype.setMap.call (this, map);
	this.overlayLayer_.setMap(map);
	if (map) this.projExtent_ = map.getView().getProjection().getExtent();
};
/** Activate or deactivate the interaction.
* @param {boolean} active
*/
ol.interaction.SnapGuides.prototype.setActive = function(active)
{	this.overlayLayer_.setVisible(active);
	ol.interaction.Interaction.prototype.setActive.call (this, active);
}
/** Clear previous added guidelines
* @param {Array<ol.Feature> | undefined} features a list of feature to remove, default remove all feature
*/
ol.interaction.SnapGuides.prototype.clearGuides = function(features)
{	if (!features) this.overlaySource_.clear();
	else
	{	for (var i=0, f; f=features[i]; i++)
		{	this.overlaySource_.removeFeature(f);
		}
	}
}
/** Get guidelines
* @return {ol.Collection} guidelines features
*/
ol.interaction.SnapGuides.prototype.getGuides = function()
{	return this.overlaySource_.getFeaturesCollection();
}
/** Add a new guide to snap to
* @param {Array<ol.coordinate>} v the direction vector
* @return {ol.Feature} feature guide
*/
ol.interaction.SnapGuides.prototype.addGuide = function(v, ortho) {
	if (v)
	{	var map = this.getMap();
		// Limit extent
		var extent = map.getView().calculateExtent(map.getSize());
		var guideLength = Math.max(
			this.projExtent_[2] - this.projExtent_[0],
			this.projExtent_[3] - this.projExtent_[1]
		);
		extent = ol.extent.buffer(extent, guideLength * 1.5);
		//extent = ol.extent.boundingExtent(extent, this.projExtent_);
		if (extent[0]<this.projExtent_[0]) extent[0]=this.projExtent_[0];
		if (extent[1]<this.projExtent_[1]) extent[1]=this.projExtent_[1];
		if (extent[2]>this.projExtent_[2]) extent[2]=this.projExtent_[2];
		if (extent[3]>this.projExtent_[3]) extent[3]=this.projExtent_[3];
		var dx = v[0][0] - v[1][0];
		var dy = v[0][1] - v[1][1];
		var d = 1 / Math.sqrt(dx*dx+dy*dy);
		var generateLine = function(loopDir) {
			var p, g = [];
			var loopCond = guideLength*loopDir*2;
			for (var i=0; loopDir > 0 ? i < loopCond : i > loopCond; i+=(guideLength * loopDir) / 100) {
				if (ortho) p = [ v[0][0] + dy*d*i, v[0][1] - dx*d*i];
				else p = [ v[0][0] + dx*d*i, v[0][1] + dy*d*i];
				if (ol.extent.containsCoordinate(extent, p)) g.push(p);
				else break;
			}
			return new ol.Feature(new ol.geom.LineString([g[0], g[g.length-1]]));
		}
		var f0 = generateLine(1);
		var f1 = generateLine(-1);
		this.overlaySource_.addFeature(f0);
		this.overlaySource_.addFeature(f1);
		return [f0, f1];
	}
};
/** Add a new orthogonal guide to snap to
* @param {Array<ol.coordinate>} v the direction vector
* @return {ol.Feature} feature guide
*/
ol.interaction.SnapGuides.prototype.addOrthoGuide = function(v)
{	return this.addGuide(v, true);
};
/** Listen to draw event to add orthogonal guidelines on the first and last point.
* @param {_ol_interaction_Draw_} drawi a draw interaction to listen to
* @api
*/
ol.interaction.SnapGuides.prototype.setDrawInteraction = function(drawi) {
	var self = this;
	// Number of points currently drawing
	var nb = 0;
	// Current guidelines
	var features = [];
	function setGuides(e) {
		var coord = e.target.getCoordinates();
		var s = 2;
		switch (e.target.getType()) {
			case 'Polygon':
				coord = coord[0].slice(0, -1);
				break;
			default: break;
		}
		var l = coord.length;
		if (l === s && self.enableInitialGuides_) {
			var x = coord[0][0];
			var y = coord[0][1];
			coord = [[x, y], [x, y - 1]];
		}
		if (l != nb && (self.enableInitialGuides_ ? l >= s : l > s)) {
			self.clearGuides(features);
			var p1 = coord[l - s], p2 = coord[l - s - 1];
			if (l > s && !(p1[0] === p2[0] && p1[1] === p2[1])) {
				features = self.addOrthoGuide([coord[l - s], coord[l - s - 1]]);
			}
			features = features.concat(self.addGuide([coord[0], coord[1]]));
			features = features.concat(self.addOrthoGuide([coord[0], coord[1]]));
			nb = l;
		}
	}
	// New drawing
	drawi.on ("drawstart", function(e) {
		// When geom is changing add a new orthogonal direction 
		e.feature.getGeometry().on("change", setGuides);
	});
	// end drawing / deactivate => clear directions
	drawi.on (["drawend", "change:active"], function(e) {
		self.clearGuides(features);
		if (e.feature) e.feature.getGeometry().un("change", setGuides);
		nb = 0;
		features = [];
	});
};
/** Listen to modify event to add orthogonal guidelines relative to the currently dragged point
* @param {_ol_interaction_Modify_} modifyi a modify interaction to listen to
* @api
*/
ol.interaction.SnapGuides.prototype.setModifyInteraction = function (modifyi) {
	function mod(d, n) {
		return ((d % n) + n) % n;
	}
	var self = this;
	// Current guidelines
	var features = [];
	function computeGuides(e) {
		var selectedVertex = e.target.vertexFeature_
		if (!selectedVertex) return;
		var f = e.target.getModifiedFeatures()[0];
		var geom = f.getGeometry();
		var coord = geom.getCoordinates();
		switch (geom.getType()) {
			case 'Polygon':
				coord = coord[0].slice(0, -1);
				break;
			default: break;
		}
		var modifyVertex = selectedVertex.getGeometry().getCoordinates();
		var idx = coord.findIndex(function(c) {
			return c[0] === modifyVertex[0] && c[1] === modifyVertex[1]
		});
		var l = coord.length;
		self.clearGuides(features);
		features = self.addOrthoGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]]);
		features = features.concat(self.addGuide([coord[mod(idx - 1, l)], coord[mod(idx - 2, l)]]));
		features = features.concat(self.addGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]]));
		features = features.concat(self.addOrthoGuide([coord[mod(idx + 1, l)], coord[mod(idx + 2, l)]]));
	}
	function setGuides(e) {
		// This callback is called before ol adds the vertex to the feature, so
		// defer a moment for openlayers to add the new vertex
		setTimeout(computeGuides, 0, e);
	}
	function drawEnd() {
		self.clearGuides(features);
		features = [];
	}
	// New drawing
	modifyi.on("modifystart", setGuides);
	// end drawing, clear directions
	modifyi.on("modifyend", drawEnd);
};
