var polyOverWeight = 14;
var polyOpacity = 1;

function RouteManager() {
	this.selectedUid = null;
}

RouteManager.prototype.getMarker = function() { throw 'override RouteManager.getMarker()!'; }
RouteManager.prototype.getActiveCats = function() { throw 'override RouteManager.getActiveCats()!'; }

RouteManager.prototype.setMap = function(map) {
	this.map = map;
}

RouteManager.prototype.setRoutes = function(routes) {
	this.routes = routes;
}

RouteManager.prototype._prepare = function(){
	this.edge2seg = {};
	this.seg2route = {};
	this.segMap = {};
	this.routeMap = {};
	this.polyHighlighters = {};
	this.selector = new ObjectSelector();	
	if (!RouteManager.GPolyMouseSensorCreated) {
		GPolyMouseSensor.createClass();
		RouteManager.GPolyMouseSensorCreated = true;
	}
	this.sensor = new GPolyMouseSensor('sensor');
	this.sensor.override({
		lineWeight: polyOverWeight,
		onEdgeOver: this.onEdgeOver.bind(this),
		onEdgeOut: this.onEdgeOut.bind(this),
		onEdgeClick: this.onEdgeClick.bind(this)
	});
}

// TODO: optimize to redraw only changed stuff
RouteManager.prototype.refresh = function(reason) {
	if (! this.edge2seg)
		reason = REASON_INIT;
		/*
	switch (reason) {
		case REASON_INIT:
		case REASON_DATA:
		case REASON_SWITCH:
		case REASON_CAT_FILTER:
			break;
		default:
			return; // nothing to do
	}
	*/

	if (this.edge2seg)
		this.destroy();

	this._prepare();

	var activeCats = this.getActiveCats();

	var zoom = this.map.getZoom();
	var bounds = xmap.getSmartProjectionBounds(zoom);
	var proj = G_NORMAL_MAP.getProjection();

	// creating stuff for routes
	for (var i = 0; i < this.routes.length; i++) {
		var route = this.routes[i];
		this.fillRouteInfo(route);
		var haveSegs = false;
		if (route.segs) {
			for (var j = 0; j < route.segs.length; j++) {
				var seg = route.segs[j];
				if (!activeCats[seg.cat] || !seg.edges) 
					continue;
				haveSegs = true;
				for (var k = 0; k < seg.edges.length; k++) {
					var edge = seg.edges[k];
					var m1 = this.getMarker(edge.p1);
					var m2 = this.getMarker(edge.p2);
					var p1 = proj.fromLatLngToPixel(new GLatLng(m1.lat, m1.lng), zoom);
					var p2 = proj.fromLatLngToPixel(new GLatLng(m2.lat, m2.lng), zoom);
					edge._v = this.edgeIntersectsWithBounds(p1.x, p1.y, p2.x, p2.y, bounds);
				}
			}
			if (haveSegs) {
				this.addRouteToSensor(route, activeCats);
				this.addRouteToHighlighters(route, activeCats);
			}
		}
	}
	this.map.addOverlay(this.sensor);
	
	if (this.selectedUid)
		this.selectObject(this.selectedUid);
}

RouteManager.prototype.destroy = function() {
	for (var key in this.polyHighlighters) {
		var hl = this.polyHighlighters[key];
		hl.destroy();
	}
	this.polyHighlighters = null;

	if (this.sensor) {
		this.map.removeOverlay(this.sensor);
		this.sensor = null;
	}

	this.edge2seg = null;
	this.seg2route = null;
	this.segMap = null;
	this.routeMap = null;
	this.selector = null;
	this.polyHighlighters = null;
}

RouteManager.prototype.addRouteToSensor = function(route, activeCats) {
	if (! route.segs)
		return;
	for (var i = 0; i < route.segs.length; i++) {
		var seg = route.segs[i];
		if (! activeCats[seg.cat] || ! seg.edges)
			continue;
		for (var j = 0; j < seg.edges.length; j++) {
			var edge = seg.edges[j];
			edge.title = edge.t;
			this.sensor.addEdge({
				uid: edge.uid,
				title : seg.t,
				p1 : this.getMarker(edge.p1),
				p2 : this.getMarker(edge.p2)
			});
		}
	}
}
RouteManager.prototype.addRouteToHighlighters = function(route, activeCats) {
	var hls = [];
	if (! route.segs)
		return;
		
	var hl;
	for (var i = 0; i < route.segs.length; i++) {
		var seg = route.segs[i];
		if (! activeCats[seg.cat] || ! seg.edges)
			continue;

		// adding segment highlighters
		hl = null;
		if (seg.edges.length == 1) {
			if (seg.edges[0]._v) {
				hl = new PolyHighlighter(
					[
						this.getMarker(seg.edges[0].p1),
						this.getMarker(seg.edges[0].p2)
					], seg.cat
				);
			}	
		}
		else {
			hl = this.buildSegHighlighter(seg);
		}
		if (hl) {
			this.polyHighlighters[seg.uid] = hl;
			hls.push(hl);
		}
	}
	// if route has more than one segment, add a route highlighter
	switch (hls.length) {
		case 0: return;
		case 1: this.polyHighlighters[route.uid] = hls[0]; break;
		default : 
			var g = new PolyHighlighterGroup(hls);
			this.polyHighlighters[route.uid] = g;
	}
	this.polyHighlighters[route.uid].highlight(0);
}

/* fills edge2seg and seg2route */
RouteManager.prototype.fillRouteInfo = function(route) {
	this.routeMap[route.uid] = route;
	if (! route.segs)
		return;
	for (var i = 0; i < route.segs.length; i++) {
		var seg = route.segs[i];
		this.segMap[seg.uid] = seg;
		this.seg2route[seg.uid] = route;
		if (! seg.edges)
			continue;
		for (var j = 0; j < seg.edges.length; j++) {
			var edge = seg.edges[j];
			this.edge2seg[edge.uid] = seg;
		}		
	}
}

RouteManager.prototype.buildSegHighlighter = function(seg) {
	if (! seg.edges)
		return;
	var points;
	var _this = this;
	function addPoint(puid){
		points.push(_this.getMarker(puid));
	}

	var edges = {};
	var i;
	
	// build edge map;
	var edgeCount = 0;
	for (i = 0; i < seg.edges.length; i++) {
		var edge = seg.edges[i];
		if (edge._v) {
			edges[edge.uid] = edge;
			edgeCount++;
		}
	}
	// if no visible edges no highlighter should be created
	if (! edgeCount)
		return null;

	var hls = [];

	// iterate until all contiguous parts are found	(no more edges left in 'edges')
	do {
		var traversedEdges = [];
		points = [];

		var pmap = {};
		for (var euid in edges) {
			var edge = edges[euid];
			
			if (pmap[edge.p1]) 
				pmap[edge.p1].push(edge);
			else 
				pmap[edge.p1] = [edge];
			
			if (pmap[edge.p2]) 
				pmap[edge.p2].push(edge);
			else 
				pmap[edge.p2] = [edge];
		}
		
		// find a marginal point
		var puid;
		for (puid in pmap) {
			if (pmap[puid].length == 1) 
				break;
		}
		puid = parseInt(puid);// array keys get converted to strings
		var processedEdge = null;
		while (true) {
			// find the edge of current point other than already processed edge				
			var p = pmap[puid];
			// break the loop if this is the last point at the margin
			if (p.length == 1 && processedEdge) 
				break;
			if (p[0] !== processedEdge)
				edge = p[0];
			else
				edge = p[1];
			
			// from the found edge find the next point other than current point
			if (edge.p1 === puid) {
				nextPuid = edge.p2;
			}
			else {
				nextPuid = edge.p1;
			}
			
			// add current point to point array
			addPoint(puid);
			
			puid = nextPuid;
			processedEdge = edge;
			traversedEdges.push(edge.uid);
		}
		// add the very last point
		addPoint(puid);
		
		var hl = new PolyHighlighter(points, seg.cat);
		hls.push(hl);
		
		// remove traversed edges from edge map
		for (i = 0; i < traversedEdges.length; i++) {
			delete edges[traversedEdges[i]];
			edgeCount--;
		}
	} while (edgeCount > 0);
	
	if (hls.length == 1)
		return hls[0];
	else
		return new PolyHighlighterGroup(hls);
}

RouteManager.prototype.getRouteCoord = function(routeUid){
	var segs = this.routeMap[routeUid].segs;
	var seg = segs[parseInt(segs.length / 2)] // select middle seg
	return this.getSegCoord(seg.uid);
}
RouteManager.prototype.getSegCoord = function(segUid){
	var seg = this.segMap[segUid];
	return this._getMidCoord(seg.edges[0].p1, seg.edges[0].p2);
}

RouteManager.prototype._getMidCoord = function (muid1, muid2) {
	var m1 = this.getMarker(muid1);
	var m2 = this.getMarker(muid2);
    var p1 = this.map.fromLatLngToDivPixel(new GLatLng(m1.lat, m1.lng));
    var p2 = this.map.fromLatLngToDivPixel(new GLatLng(m2.lat, m2.lng));
	var mid = new GPoint();
	mid.x = parseInt((p1.x + p2.x) / 2);
	mid.y = parseInt((p1.y + p2.y) / 2);
	coord = this.map.fromDivPixelToLatLng(mid);
	return coord;	
}

// TODO: respect selection mode, selection modes to be implemented
RouteManager.prototype.getObjectForEdge = function(edgeuid) {
	return this.edge2seg[edgeuid].uid;
}

RouteManager.prototype.edgeIntersectsWithBounds = function(x1, y1, x2, y2, bounds) {
	/* quick check if guaranteed non-intersect */
	var minX = Math.min(x1, x2);
	if (minX > bounds.maxX)
		return false;
	var maxX = Math.max(x1, x2);
	if (maxX < bounds.minX)
		return false;
	var minY = Math.min(y1, y2);
	if (minY > bounds.maxY)
		return false;
	var maxY = Math.max(y1, y2);
	if (maxY < bounds.minY)
		return false;
	
	// find out the edge equation
	var a;
	if (x1 == x2)
		a = 0;
	else
		a = (y2 - y1) / (x2 - x1);
		
	var b = y1 - a * x1;

	// find out whether points lay on both sides from the edge	
	var lastSide = 0;
	function checkSides(x, y) {
		var side = a * x + b - y;
		if (side * lastSide < 0) // differend signs
			return true; // have points on both sides
		lastSide = side;
		return false;
	}
	return (
		checkSides(bounds.minX, bounds.minY) ||
		checkSides(bounds.maxX, bounds.maxY) ||
		checkSides(bounds.minX, bounds.maxY) ||
		checkSides(bounds.maxX, bounds.minY)
	);
}

RouteManager.prototype.onEdgeOver = function(edgeuid) {
	var uid = this.getObjectForEdge(edgeuid);
	if (this.onObjectOver(uid))
		this.selector.over(this.polyHighlighters[uid]);
}
RouteManager.prototype.onEdgeOut = function(edgeuid) {
	var uid = this.getObjectForEdge(edgeuid);
	if (this.onObjectOut(uid))
		this.selector.out(this.polyHighlighters[uid]);
}
RouteManager.prototype.onEdgeClick = function(edgeuid) {
	var uid = this.getObjectForEdge(edgeuid);
	if (this.onObjectSelect(uid))
		this.selectObject(uid);
}

RouteManager.prototype.onObjectOver = function(uid) {
	return true;
}
RouteManager.prototype.onObjectOut = function(uid) {
	return true;
}
RouteManager.prototype.onObjectSelect = function(uid) {
	return true;
}

RouteManager.prototype.selectObject = function(uid) {
	this.selectedUid = uid;
	var hl = this.polyHighlighters[uid];
	this.selector.select(hl);
}

function PolyHighlighter(points, cat, options) {
	if (points)
		this.points = points;
	this.cat = cat;
	if (options)
		this.options = options;
	this.flag = -1;
	this.ovls = {};
}

PolyHighlighter.prototype.options = {
	weight0 : 3,
	weight1 : 5,
	weight2 : 7,
	defaultColor: '#000000'
};

PolyHighlighter.prototype.highlight = function(flag) {
	if (flag == this.flag)
		return;

	var color;
	var c = this.getCatInfo(this.cat);
	if (c && c.color)
		color = c.color;
	else
		color = this.options.defaultColor;

	var weight;
	switch (flag) {
		case 0: weight = this.options.weight0; break;
		case 1: weight = this.options.weight1; break;
		case 2: weight = this.options.weight2; break;
	}

	var coords = [];
	for (var i = 0; i < this.points.length; i++) {
		var p = this.points[i];
		coords.push(new GLatLng(p.lat, p.lng));
	}
	if (! this.ovls[flag]) {
		this.ovls[flag] = new GPolyline(coords, color, weight, polyOpacity);
		// this.ovls[flag].enableEditing(true);
		map.addOverlay(this.ovls[flag]);
	}
	this.ovls[flag].show();
	if (this.ovls[this.flag]) {
		this.ovls[this.flag].hide();
	}
	this.flag = flag;
}

PolyHighlighter.prototype.destroy = function() {
	for (key in this.ovls) {
		var ovl = this.ovls[key];
		map.removeOverlay(ovl);
		this.flag = -1;
	}
}
PolyHighlighter.prototype.getCatInfo = function() { return {color:'black'}; }

PolyHighlighterGroup = function(hls) {
	this.hls = hls;	
}
PolyHighlighterGroup.prototype.highlight = function(flag) {
	for (var i = 0; i < this.hls.length; i++)
		this.hls[i].highlight(flag);
}
PolyHighlighterGroup.prototype.destroy = function(flag) {
	for (var i = 0; i < this.hls.length; i++)
		this.hls[i].destroy(flag);
}
