// Log level class
function LogLevel(level, description){
	this.level = level;
	this.description = description;
}
LogLevel.prototype.level = null;
LogLevel.prototype.description = "";
LogLevel.prototype.toString = function(){
	return this.description;
}



// Log item class
function LogItem(level, message){
	this.level = level;
	this.message = message;
	this.time = new Date();
}
LogItem.prototype.level = null;
LogItem.prototype.time = null;
LogItem.prototype.message = "";
LogItem.prototype.toString = function(){
	return this.level + ": " + this.message;
}



// Logger class
function Logger(){
	this.init();
}
Logger.prototype.prepend = true;
Logger.prototype.logLevels = new Array();
Logger.prototype.logItems = new Array();
// Format an individual log item for HTML output
Logger.prototype.formatForOutput = function(logitem){
	var outArr = new Array();
	outArr.push("<div class=\"logitem level_" + logitem.level + "\">");
	outArr.push("<div class=\"level\">" + logitem.level + "</div>");
	outArr.push("<div class=\"time\">" + logitem.time + "</div>");
	outArr.push("<div class=\"description\">" + logitem.message + "</div>");
	outArr.push("</div>");
	
	return outArr.join("\n");
}
// Return a complete log HTML
Logger.prototype.getHTMLOutput = function(){
	var output = new Array();
	for(var i = 0; i < this.logItems.length; i++){
		output.push(this.formatForOutput(this.logItems[i]));
	}
	if(this.prepend == true)
		output.reverse();
	return output.join("");
}
// Initialize log levels
Logger.prototype.init = function(){
	this.logLevels.push(new LogLevel(0, "Notice"));
	this.logLevels.push(new LogLevel(1, "Warning"));
	this.logLevels.push(new LogLevel(2, "Error"));
}
// Add a log item into the log array
Logger.prototype.log = function(levelIdx, description){
	if(typeof(this.logLevels[levelIdx]) == 'object'){
		this.logItems.push(new LogItem(this.logLevels[levelIdx], description)); 
	}
	else{
		alert("Invalid log level.");
	}
}
// Clear log
Logger.prototype.clear = function(){
	this.logItems = new Array();
}





// Utility object to be used with Google Maps
var MapUtil = {
	emtId: "map",
	logEmt: null,
	gmapsObj: null,
	geocoder: new GClientGeocoder(),
	logger: new Logger(),
	zoom: 11,
	markers: new Array(),
	currentRoute: null,
	directionsObj: new GDirections(),
	polyline: null,
	
	// Default marker options
	markerOpts: {icon: new GIcon(G_DEFAULT_ICON)},
	
	// Output log HTML to log container
	outputLog: function(){
		this.logEmt.innerHTML = this.logger.getHTMLOutput();
	},
	
	// Initialize map
	init: function(geoPt){
		this.logger.log(0, "Init MapUtil: " + this.emtId);
		this.gmapsObj = new GMap2(document.getElementById(this.emtId));
		this.gmapsObj.setCenter(geoPt, this.zoom);
		this.gmapsObj.checkResize();
		this.gmapsObj.setCenter(geoPt, this.zoom);
	},
	
	/*
		Add marker to map
		Parameters:
			@geoPt: GLatLng object
			[@opts]: GMarkerOptions object
	*/
	addMarker: function(geoPt, opts){
		this.logger.log(0, "Add marker: " + geoPt);
		var optObj = this.markerOpts;
		
		if(typeof(opts) == 'object'){
			this.logger.log(0, "Overriding default marker options");
			optObj = opts;
		}
		
		var marker = new GMarker(geoPt, optObj);
		this.gmapsObj.addOverlay(marker);
		this.markers.push(marker);
		return marker;
	},
	
	// Pan map to given GLatLng coordinate
	panTo: function(geoPt){
		this.logger.log(0, "Pan to: " + geoPt);
		this.mapObject.panTo(geoPt);
	},
	
	/*
		Geocode given address
		Parameters:
			@addr: Address as string
			@callback: Callback function to be called by the GClientGeocoder with the geocoded GLatLng parameter
			[@sanitize]: Sanitize address before geocoding
	*/
	geocodeAddress: function(addr, callback, sanitize){
		var address = addr;
		if(sanitize)
			address = this.sanitizeString(addr);
			
		this.logger.log(0, "Geocoding: " + address);
		if (this.geocoder) {			
			this.geocoder.getLatLng(
				address,
				function(geoPt){
					if(geoPt != null)
						MapUtil.logger.log(0, "Geocoding successful");
					else
						MapUtil.logger.log(1, "Geocoding failed");
					callback(geoPt);
				}
			);
		}
	},
	
	/*
		Load directions from address to address
		Parameters:
			@from: Starting address
			@to: Ending address
			@callback: Callback function to be executed after the directions have been loaded
			[@opts]: Not used ATM
	*/
	getDirections: function(from, to, callback, opts){
		this.directionsObj = new GDirections(this.gmapsObj);
		GEvent.addListener(this.directionsObj, "load", function(){
			callback();
		});
		this.directionsObj.load("from: " + this.sanitizeString(from) + " to: " + this.sanitizeString(to));
		GEvent.addListener(this.directionsObj, "error", function() {
			alert("GDirections error.");
			document.getElementById("btn_newRoute").disabled = false;
			document.getElementById("btn_closeRoute").disabled = true;
			document.getElementById("btn_editMode").disabled = true;
			document.getElementById("btn_simplify").disabled = true;
			document.getElementById("btn_addMarker").disabled = true;
			window.location.reload();
		});
	},
	
	// Clear all map overlays
	clearLayers: function(){
		this.logger.log(0, "Clear layers");
		this.gmapsObj.clearOverlays();
	},
	
	// Sanitize string
	sanitizeString: function(str){
		this.logger.log(0, "Sanitize string");
		var newStr = str.replace(/&amp;/g, '&').replace(/&nbsp;/g, ' ');
		this.logger.log(0, "Sanitization result: " + newStr);
		return newStr;
	},
	
	/*
		Build a polyline from an array of points
		Parameters:
			@arr: An array of coordinate object literals ({lat: [latitude], lng: [longitude]})
			@color: Polyline color (#RRGGBB)
			@weight: Polyline weight in pixels
			@opacity: Polyline opacity (0 - 1)
			[@opts]: GPolylineOptions object
			[@createRoute]: Replace the current route with the newly created polyline (boolean)
	*/
	buildPolylineFromArray: function(arr, color, weight, opacity, opts, createRoute){
		var gpoints = new Array();
		
		for(var i = 0; i < arr.length; i++){
			var point = arr[i];
			this.logger.log(0, "Add polyline vertex: " + point.lat + ", " + point.lng);
			gpoints.push(new GLatLng(point.lat, point.lng));
		}
		
		this.polyline = new GPolyline(gpoints, color, weight, opacity, opts);
		
		if(typeof(createRoute) != 'undefined' && createRoute){
			this.currentRoute = new MapRoute();
			this.currentRoute.buildFromPolyline(this.polyline);
		}
		this.gmapsObj.addOverlay(this.polyline);
		this.logger.log(0, "Total polyline vertices " + arr.length);
	},
	
	/*
		Build a route from polyline
		Parameters:
			@polyline: GPolyline object
	*/
	buildFromPolyline: function(polyline){
		this.polyline = polyline;
		this.currentRoute.buildFromPolyline(this.polyline);
		this.gmapsObj.addOverlay(this.polyline);
	}
}




/*
	Map route class
	This class is instantiated by the MapUtil object as MapUtil.currentRoute
*/
function MapRoute(){}
MapRoute.prototype.points = new Array(); // Polyline vertices (GLatLng[])
MapRoute.prototype.markers = new Array(); // Route markers (object literals {geoPt: GLatLng, opts: GMarkerOptions, info: {name: String, nickname: String, style: String, type: String}})
// Icon defaults. Not used ATM
MapRoute.prototype.icons = {
	start: new GIcon(G_DEFAULT_ICON),
	finish: new GIcon(G_DEFAULT_ICON),
	point: new GIcon(G_DEFAULT_ICON)
}
// Add polyline vertices to the points array
MapRoute.prototype.buildFromPolyline = function(polyline){
	this.points = new Array();
	for(var i = 0; i < polyline.getVertexCount(); i++){
		this.points.push(polyline.getVertex(i));
	}
}
// Return all vertices
MapRoute.prototype.getPoints = function(){
	return this.points;
}
// Return first vertex
MapRoute.prototype.getStart = function(){
	if(this.points.length > 0)
		return this.points[0];
	else
		return false;
}
// Return last vertex
MapRoute.prototype.getFinish = function(){
	if(this.points.length > 0)
		return this.points[this.legs.length - 1];
	else
		return false;
}
// Find a polyline vertex nearest to given GLatLng
MapRoute.prototype.findNearestPoint = function(geoPt){
	MapUtil.logger.log(0, "Find point nearest to " + geoPt);
	var nearestPt = false;
	for(var i = 0; i < this.points.length; i++){
		if(!nearestPt || this.points[i].distanceFrom(geoPt) < nearestPt.distanceFrom(geoPt))
			nearestPt = this.points[i];
	}
	return nearestPt;
}
// Add marker to map and markers array
MapRoute.prototype.addMarker = function(geoPt, opts){
	MapUtil.logger.log(0, "Add marker " + geoPt);
	this.markers.push({geoPt: geoPt, opts: opts, info: {name: "", nickname: "", style: "default", type: "poi"}});
	var marker = new GMarker(geoPt);
	MapUtil.gmapsObj.addOverlay(marker);
	MapUtil.logger.log(0, "Attach click listener to marker at " + geoPt);
	var listener = GEvent.addListener(marker, "click", function(latlng){MapEditor.showMarkerDetails(latlng);});
}
// Add marker to map and markers array
MapRoute.prototype.addBindableMarker = function(geoPt, opts){
	MapUtil.logger.log(0, "Add marker " + geoPt);
	this.markers.push({geoPt: geoPt, opts: opts, info: {name: "", nickname: "", style: "default", type: "poi"}});
	var marker = new GMarker(geoPt, opts);
	MapUtil.gmapsObj.addOverlay(marker);
	return marker;
}
// Remove marker from the marker array by index
MapRoute.prototype.removeMarker = function(markerIdx){
	if(markerIdx >= 0 && markerIdx < this.markers.length){
		this.markers.splice(markerIdx, 1);
	}
}
// Find marker nearest to given GLatLng
MapRoute.prototype.findNearestMarker = function(geoPt){
	MapUtil.logger.log(0, "Find marker nearest to " + geoPt);
	var nearestMarker = false;
	var nearestIndex = null;
	for(var i = 0; i < this.markers.length; i++){
		if(!nearestMarker || this.markers[i].geoPt.distanceFrom(geoPt) < nearestMarker.geoPt.distanceFrom(geoPt)){
			nearestMarker = this.markers[i];
			nearestIndex = i;
		}
	}
	
	return {marker: nearestMarker, index: nearestIndex};
}
// Clear points and markers from arrays
MapRoute.prototype.clearAll = function(){
	this.points = new Array();
	this.markers = new Array();
}
/*
	Update marker info
	Parameters:
		@idx: Marker index in array
		@infoObj: New marker info object literal ({name: String, nickname: String, style: String})
*/
MapRoute.prototype.updateInfo = function(idx, infoObj){
	this.markers[idx].info = infoObj;
}