var geoAPI = new function() {

	this.mapURL = 'http://4geo.ru/maps/light?remoteURL=' + window.location;
	
};

var Base64 = {
	// private property
	_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
 
	// public method for encoding
	encode : function (input) {
		var output = "";
		var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
		var i = 0;
 
		input = Base64._utf8_encode(input);
 
		while (i < input.length) {
 
			chr1 = input.charCodeAt(i++);
			chr2 = input.charCodeAt(i++);
			chr3 = input.charCodeAt(i++);
 
			enc1 = chr1 >> 2;
			enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
			enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
			enc4 = chr3 & 63;
 
			if (isNaN(chr2)) {
				enc3 = enc4 = 64;
			} else if (isNaN(chr3)) {
				enc4 = 64;
			}
 
			output = output +
			this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
			this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
 
		}
 
		return output;
	},
 
	// public method for decoding
	decode : function (input) {
		var output = "";
		var chr1, chr2, chr3;
		var enc1, enc2, enc3, enc4;
		var i = 0;
 
		input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
 
		while (i < input.length) {
 
			enc1 = this._keyStr.indexOf(input.charAt(i++));
			enc2 = this._keyStr.indexOf(input.charAt(i++));
			enc3 = this._keyStr.indexOf(input.charAt(i++));
			enc4 = this._keyStr.indexOf(input.charAt(i++));
 
			chr1 = (enc1 << 2) | (enc2 >> 4);
			chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
			chr3 = ((enc3 & 3) << 6) | enc4;
 
			output = output + String.fromCharCode(chr1);
 
			if (enc3 != 64) {
				output = output + String.fromCharCode(chr2);
			}
			if (enc4 != 64) {
				output = output + String.fromCharCode(chr3);
			}
 
		}
 
		output = Base64._utf8_decode(output);
 
		return output;
 
	},
 
	// private method for UTF-8 encoding
	_utf8_encode : function (string) {
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";
 
		for (var n = 0; n < string.length; n++) {
 
			var c = string.charCodeAt(n);
 
			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}
 
		}
 
		return utftext;
	},
 
	// private method for UTF-8 decoding
	_utf8_decode : function (utftext) {
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;
 
		while ( i < utftext.length ) {
 
			c = utftext.charCodeAt(i);
 
			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			}
 
		}
 
		return string;
	}
 
};


(function() {
	var initializing = false;
	var fnTest = /xyz/.test(function() { xyz; }) ? /\b_super\b/ : /.*/;
	this.Class = function() {};

	Class.extend = function(prop) {
		var _super = this.prototype;

		initializing = true;
		var prototype = new this();
		initializing = false;

		for (var name in prop) {
			prototype[name] = typeof prop[name] == "function"
					&& typeof _super[name] == "function"
					&& fnTest.test(prop[name]) ? (function(name, fn) {
				return function() {
					var tmp = this._super;
					this._super = _super[name];
					var ret = fn.apply(this, arguments);
					this._super = tmp;

					return ret;
				};
			})(name, prop[name]) : prop[name];
		}

		function Class() {
			if (!initializing && this.init)
				this.init.apply(this, arguments);
		}

		Class.prototype = prototype;
		Class.constructor = Class;
		Class.extend = arguments.callee;
		return Class;
	};
})();

geoAPI.callbackNotifier = (function() {

	geoAPI.CallbackNotifier = function() {

		this.callbackStack = new Array();

	};

	geoAPI.CallbackNotifier.prototype = {

		addCallbackListener : function(object, method, runOnce) {
			this.callbackStack.push( {
				'object' : object,
				'method' : method,
				'runOnce': runOnce
			});
		},
		
		removeCallbackListeners: function(object, method) {
			for ( var i = 0; i < this.callbackStack.length; i++) {
				if (this.callbackStack[i].object == object && this.callbackStack[i].method == method) {
					this.callbackStack.splice(i, 1);
				} 
			}
		},
		
		removeAllObjectListeners: function(id) {
			for ( var i = 0; i < this.callbackStack.length; i++) {
				if (this.callbackStack[i].id == id) {
					this.callbackStack.splice(i, 1);
				} 
			}
		},

		notify : function(method, params) {
			var methodName = 'on' + method.toLowerCase();
			if (params) {
				var resultStack = new Array();
				for (var i = 0; i < this.callbackStack.length; i++) {
					if (this.callbackStack[i].object.id == params.id 
								&& this.callbackStack[i].method == methodName) {
						if (!this.callbackStack[i].runOnce) {
							resultStack.push(this.callbackStack[i]);
						}
						if (this.callbackStack[i].object[methodName]) {
							this.callbackStack[i].object[methodName](this.callbackStack[i].object, params);
						}
					} else {
						resultStack.push(this.callbackStack[i]);
					}
				}
				this.callbackStack = resultStack;
			} 
		}

	};

	return new geoAPI.CallbackNotifier();

})();

geoAPI.rpc = (function() {

	geoAPI.RPC = function() {

		this.activate();
		
	};

	geoAPI.RPC.prototype = {

		activate : function() {
			if (window.addEventListener) {
				window.addEventListener("message", this.listenCallback, false);
			} else {
				window.attachEvent("onmessage", this.listenCallback);
			}
		},

		deactivate : function() {
			if (window.removeEventListener) {
				window.removeEventListener("message", this.listenCallback,
						false);
			} else {
				window.detachEvent("onmessage", this.listenCallback);
			}
		},
		
		listenCallback : function(event) {
			if (geoAPI.mapURL.indexOf(event.origin) == 0) {
				var dataObject = eval('('+event.data+')');
				if (!dataObject.params.isExternalMethod) {
					geoAPI.callbackNotifier.notify(dataObject.name,	dataObject.params);
				} else {
					geoAPI.rpc.callExternalMethod(dataObject.params);
				}
			}
		},
		
		call : function(element, method, params) {
			var eventOptions = {
				'name' : method,
				'params' : params
			};
			element.contentWindow.postMessage(window.JSON.stringify(eventOptions), geoAPI.mapURL);
		},
		
		callExternalMethod: function(options) {
			window[options.methodName](options.params);
		}

	};

	return new geoAPI.RPC();

})();


geoAPI.photos = (function() {

	geoAPI.Photos = function() {
		
		this.photoUrl = 'http://img.4geo.ru/photos/index.php';

	};

	geoAPI.Photos.prototype = {
		
		getPhotoByAddress: function(town, street, building, type) {
			var request = '';
			if (town && street && building) {
				request = this.photoUrl + '?town=' + town + '&street=' + street + '&building=' + building;
				request += '&type=' + ((type) ? type : 'original');
			}
			return request;
		}
		
	};

	return new geoAPI.Photos();

})();


geoAPI.maps = new function() {
	
};

geoAPI.maps.Map = function(containerElement, options, callback) {
	
	this.id = Math.floor(Math.random() * 10000);

	this.mapContainer = new geoAPI.maps.MapContainer(containerElement);

	this.locationService = new geoAPI.maps.LocationService(this.mapContainer);
	
	this.flyService = new geoAPI.maps.FlyService(this);
	
	this.markers = new Array();
	
	this.markersGroups = new Array();

	this.initMap(options, callback);
	
};

geoAPI.maps.Map.prototype = {

	initMap : function(options, callback) {
		this.locateAddress(options);
		if (callback) {
			geoAPI.maps.event.addListener(this, "MapRendered", callback);
		}
	},

	locateAddress : function(options, callback) {
		this.locationService.locateAddress(this, options, callback);
	},

	locateTown : function(options, callback) {
		this.locationService.locateTown(this, options, callback);
	},

	locateStreet : function(options, callback) {
		this.locationService.locateStreet(this, options, callback);
	},

	locateBuilding : function(options, callback) {
		this.locationService.locateBuilding(this, options, callback);
	},
	
	addMarker: function(marker) {
		this.markers.push(marker);
	},
	
	getMarkers: function() {
		return this.markers;
	},
	
	removeMarker: function(marker) {
		for (var i = 0; i < this.markers.length; i++) {
			if (this.markers[i] == marker) {
				this.markers.splice(i, 1);
			}
		}
	},
	
	addMarkersGroup: function(markersGroup) {
		this.markersGroups.push(markersGroup);
	},
	
	getMarkersGroups: function() {
		return this.markersGroups;
	},
	
	removeMarkersGroup: function(markersGroup) {
		for (var i = 0; i < this.markersGroups.length; i++) {
			if (this.markersGroups[i] == markersGroup) {
				this.markersGroups.splice(i, 1);
			}
		}
	},
	
	clearMarkers: function() {
		for (var i=0; i<this.markers.length; i++) {
			this.markers[i].setMap(null);
		}
		this.markers.splice(0, this.markers.length - 1);
	},
	
	addMarkersLayer: function(layerName) {
		this.callMethod('createMarkersLayer', {
			'id': this.id, 
			'options': {'layerName': layerName}
		});
	},
	
	clearLayer: function(layerName) {
		this.callMethod('clearLayer', {
			'id': this.id, 
			'options': {'layerName': layerName}
		});
	},
	
	callMethod: function(methodName, params) {
		geoAPI.rpc.call(this.mapContainer.mapWindow, methodName, params);
	}

};

geoAPI.maps.LocationService = function(mapConatainer) {

	this.town = null;

	this.mapContainer = mapConatainer;

};

geoAPI.maps.LocationService.prototype = {

	locateAddress : function(map, options, callback) {
		if (options.building && options.street) {
			this.locateBuilding(map, options, callback);
		} else if (options.street) {
			this.locateStreet(map, options, callback);
		} else {
			this.locateTown(map, options, callback);
		}
	},

	locateTown : function(map, options, callback) {
		var locationParams = {
			'town' : this.getTown(options)
		};
		this.locateByParams(map, locationParams, options, callback);
	},

	locateStreet : function(map, options, callback) {
		var locationParams = {
				'town' : this.getTown(options),
				'street' : options.street
		};
		this.locateByParams(map, locationParams, options, callback);
	},

	locateBuilding : function(map, options, callback) {
		var locationParams = {
				'town' : this.getTown(options),
				'street' : options.street,
				'number' : options.building
		};
		this.locateByParams(map, locationParams, options, callback);
	},
	
	locateByParams: function(map, locationParams, options, callback) {
		if (options.disableMouseZoom) locationParams.scrolling = !options.disableMouseZoom;
		locationParams.mapId = map.id;
		this.mapContainer.updateIframeUrl(locationParams);
		this.setLocationCallback(map, callback);
	},

	setLocationCallback : function(map, callback) {
		if (callback) {
			geoAPI.maps.event.addListener(map, "MapRendered", callback);
		}
	},

	getTown : function(options) {
		this.town = options.town ? options.town : this.town;
		if (this.town == null) {
			throw new Error('Town option is not defined for geoAPI.maps.Map');
		}
		return this.town;
	}

};

geoAPI.maps.MapContainer = function(containerElement) {

	this.mapWindow = this.createMapWindow(containerElement);

};

geoAPI.maps.MapContainer.prototype = {

	createMapWindow : function(containerElement) {
		if (containerElement && !this.mapWindow) {
			var window = document.createElement("IFRAME");
			window.setAttribute("src", Base64.encode(geoAPI.mapURL));
			window.style.width = '100%';
			window.style.height = '100%';
			containerElement.appendChild(window);
			return window;
		} else if (!containerElement) {
			throw new Error("Html page has no element to build map");
		} else if (this.mapWindow) {
			throw new Error("Map has been already initialized");
		}
	},

	updateIframeUrl : function(options) {
		if (this.mapWindow) {
			this.mapWindow.setAttribute("src", geoAPI.mapURL
					+ this.getUrlStrFromOptions(options));
		}
	},

	getUrlStrFromOptions : function(options) {
		var parametrsStr = "";
		if (options) {
			for ( var i in options)
				parametrsStr += "&" + i + "=" + encodeURIComponent(options[i]);
		}
		return parametrsStr;
	}

};

geoAPI.maps.event = (function() {

	geoAPI.maps.Event = function() {

	};

	geoAPI.maps.Event.prototype = {
			
		addListener: function(component, eventName, listener, runOnce) {
			this.registerEvent(component, "on"+eventName, listener, runOnce);
		},
		
		removeListener: function(component, eventName) {
			this.unregisterEvent(component, "on"+eventName);
		},
		
		registerEvent: function(component, eventName, listener, runOnce) {
			this.unregisterEvent(component, eventName);
			this.registerAdditionalEvent(component, eventName, listener, runOnce);
		},
		
		registerAdditionalEvent: function(component, eventName, listener, runOnce) {
			if (listener) component[eventName.toLowerCase()] = listener;
			geoAPI.callbackNotifier.addCallbackListener(component, eventName.toLowerCase(), runOnce);
		}, 
		
		unregisterEvent: function(component, eventName) {
			geoAPI.callbackNotifier.removeCallbackListeners(component, eventName.toLowerCase());
		}

	};

	return new geoAPI.maps.Event();

})();

geoAPI.maps.FlyService = function(map) {
	
	this.map = map;
	
};

geoAPI.maps.FlyService.prototype = {
		
	flyToAddress: function(address, zoom, callback) {
		address.town = this.map.locationService.town;
		geoAPI.maps.event.registerEvent(this.map, 'onFly', callback, true);
		this.map.callMethod('flyToAddress', {
			id: this.map.id,
			address: address,
			zoom: zoom
		});
	},
	
	flyToLonLat: function(lonlat, zoom, callback) {
		geoAPI.maps.event.registerEvent(this.map, 'onFly', callback, true);
		this.map.callMethod('flyToLonLat', {
			id: this.map.id,
			lonlat: lonlat,
			zoom: zoom
		});
	},
	
	zoom: function(zoom, callback) {
		geoAPI.maps.event.registerEvent(this.map, 'onZoom', callback, true);
		this.map.callMethod('zoom', {
			id: this.map.id,
			zoom: zoom
		});
	}

};

/* Map object */
geoAPI.maps.MapObject = Class.extend({
	
	id: null,
	
	options: null,
	
	map : null,
	
	isCreated: false,
	
	init: function(options) {
		this.id = this.generateId();
		this.options = options;
		if (options.map) {
			this.setMap(options.map);
		}
  	},

	setMap : function(map) {
  		this.options.map = null;
  		if ((this.map == null) && map) {
  			this.map = map;
  			this.create();
  		} else if (this.map && (map == null)) {
  			this.destroy();
  			this.map = null;
  		}
	},
	
	create: function() {
		if (this.options && this.map) {
			this.options.town = this.map.locationService.town;
		}
		geoAPI.maps.event.registerEvent(this, 'onCreate', null, true);
		this.registerEvents(this.options.events);
	},
	
	destroy : function() {
		geoAPI.maps.event.registerEvent(this, 'onDestroy', null, true);
	}, 
	
	registerEvents: function(events) {
		if (events) {
			for (var event in events) {
				geoAPI.maps.event.registerEvent(this, event, events[event]);
			}
		}
	},
	
	onCreate: function() {
		this.isCreated = true;
	},
	
	onDestroy: function() {
		geoAPI.callbackNotifier.removeAllObjectListeners(this);
		this.isCreated = false;
	},
	
	generateId : function() {
		return Math.floor(Math.random() * 10000);
	}
	
});

/* Visual object */
geoAPI.maps.VisualObject = geoAPI.maps.MapObject.extend({
	
	isVisible: false,
	
	create: function() {
		geoAPI.maps.MapObject.prototype.create.apply(this);
		geoAPI.maps.event.registerEvent(this, 'onShow');
		geoAPI.maps.event.registerEvent(this, 'onHide');
		geoAPI.maps.event.registerEvent(this, 'onClick');
		geoAPI.maps.event.registerEvent(this, 'onMouseMove');
	},
	
	destroy: function() {
		geoAPI.maps.MapObject.prototype.destroy.apply(this);
	},
	
	setMap: function(map) {
		geoAPI.maps.MapObject.prototype.setMap.apply(this, [map]);
		if (this.map) {
			this.show();
		} else {
			this.hide();
		}
	},
	
	show: function() {
		this.isVisible = true;
	},
	
	hide: function() {
		this.isVisible = false;
	},
	
	toggle: function() {
		if (isVisible) {
			this.hide();
		} else {
			this.show();
		}
	},
	
	onshow: function() {
		
	},
	
	onhide: function() {
		
	}
	
});

/* Marker */
geoAPI.maps.Marker = geoAPI.maps.VisualObject.extend({
	
	create: function() {
		geoAPI.maps.VisualObject.prototype.create.apply(this);

		this.map.callMethod('createMarker', {
			'id': this.id, 
			'options': this.options
		});
		
		this.map.addMarker(this);
	},
	
	destroy: function() {
		this.map.callMethod('destroyMarker', {
			'id': this.id 
		});
		this.map.removeMarker(this);
		geoAPI.maps.VisualObject.prototype.destroy.apply(this);
	},
	
	oncreate: function(marker, params) {
		geoAPI.maps.VisualObject.prototype.onCreate.apply(this);
		this.options.lonlat = params.lonlat;
		this.options.width = params.width;
		this.options.height = params.height;
	},
	
	show: function() {
		this.map.callMethod('showMarker', {
			'id': this.id 
		});
		geoAPI.maps.VisualObject.prototype.show.apply(this);
	},
	
	hide: function() {
		this.map.callMethod('hideMarker', {
			'id': this.id 
		});
		geoAPI.maps.VisualObject.prototype.hide.apply(this);
	}
	
});

/* MarkerGroup */
geoAPI.maps.MarkerGroup = geoAPI.maps.VisualObject.extend({
	
	markers: new Array(),
	
	create: function() {
		geoAPI.maps.VisualObject.prototype.create.apply(this);
		this.addMarkers(this.options.markers);
		this.registerDefaultEvents(this.options.defaultEvents);
		this.setDefaultImage(this.options.defaultImage);
		this.map.addMarkersGroup(this);
	},
	
	destroy: function() {
		this.clearMarkers();
		this.map.removeMarkersGroup(this);
		geoAPI.maps.VisualObject.prototype.destroy.apply(this);
	},
	
	show: function() {
		geoAPI.maps.VisualObject.prototype.show.apply(this);
		for (var i = 0; i < this.markers.length; i++) {
			this.markers[i].setMap(this.map);
		}
	},
	
	hide: function() {
		geoAPI.maps.VisualObject.prototype.hide.apply(this);
		for (var i = 0; i < this.markers.length; i++) {
			this.markers[i].hide();
		}
	},

	addMarkers: function(markers) {
		if (markers) {
			for (var i=0; i<markers.length; i++) {
				var marker = new geoAPI.maps.Marker(markers[i]);
				this.markers.push(marker);
			}
		}
	},
	
	clearMarkers: function() {
		for (var i = 0; i < this.markers.length; i++) {
			this.markers[i].destroy();
		}
		this.markers = new Array();
	},
	
	registerDefaultEvents: function(defaultEvents) {
		if (defaultEvents) {
			for (var i = 0; i < this.markers.length; i++) {
				this.markers[i].registerEvents(defaultEvents);
			}
		}
	},
	
	setDefaultImage: function(image) {
		if (image) {
			for (var i =0; i < this.markers.length; i++) {
				if (!this.markers[i].options.imageUrl) {
					this.markers[i].options.imageUrl = image.url;
					this.markers[i].options.imageWidth = image.width;
					this.markers[i].options.imageHeight = image.height;
				}
			}
		}
	}
	
});

/* InfoWindow */
geoAPI.maps.InfoWindow = geoAPI.maps.MapObject.extend({
	
	create: function() {
		geoAPI.maps.VisualObject.prototype.create.apply(this);
		this.map.callMethod('createInfoWindow', {
			'id': this.id, 
			'options': this.options
		});
	},
	
	destroy: function() {
		this.map.callMethod('destroyInfoWindow', {
			'id': this.id 
		});
		geoAPI.maps.VisualObject.prototype.destroy.apply(this);
	},
	
	open: function(owner) {
		this.options.offsetWidth = owner.options.width;
		this.options.offsetHeight = owner.options.height;
		this.options.lonlat = owner.options.lonlat;
		if (!this.isVisible && owner) {
			if (!this.map) {
				this.setMap(owner.map);
			}
			this.show();
		} 
	},
	
	close: function() {
		if (this.isVisible) this.hide();
	},
	
	show: function() {
		this.map.callMethod('showInfoWindow', {
			'id': this.id,
			'options': this.options
		});
		geoAPI.maps.event.registerEvent(this, 'onClose', this.close);
		geoAPI.maps.VisualObject.prototype.show.apply(this);
	},
	
	hide: function() {
		this.map.callMethod('hideInfoWindow', {
			'id': this.id
		});
		geoAPI.maps.VisualObject.prototype.hide.apply(this);
		geoAPI.maps.event.unregisterEvent(this, 'onClose');
	}
	
});

