( function ( ) {
	if ( Array.prototype.shuffle === undefined ) {
		Array.prototype.shuffle = function ( ) {
			return this.sort ( function ( ) {
				return 0.5 - Math.random();
			} );
		}
	}
	if ( Array.prototype.random === undefined ) {
		Array.prototype.random = function ( count ) {
			if ( !this.length ) return null;
			var c = Math.max ( Math.min ( this.length, parseInt ( count ) || 1 ), 0 );
			var r = this.shuffle().slice(0,c);
			if ( count == null ) return r[0];
			return r;
		}
	}
	if ( String.prototype.ucwords === undefined ) {
		String.prototype.ucwords = function ( ) {
			return this.replace ( /\w+/g, function ( a ) {
				return a.charAt(0).toUpperCase() + a.substring(1).toLowerCase();
			} );
		}
	}
	if ( Date.prototype.getDifference === undefined ) {
		Date.prototype.getDifference = function(nLimit, sBetween, sLastBetween, dNow){
			if(!nLimit){ nLimit = 2; }
			if(!sBetween){ sBetween = ", "; }
			if(!sLastBetween){ sLastBetween = " and "; }
			if(!dNow){ dNow = new Date(); }

			var collStructs = Date.prototype.getDifference._collStructs;
			var nSecondsRemain = (dNow.valueOf() - this.valueOf()) / 1000;
			var sReturn = "";
			var nCount = 0;
			var nFloored;

			for(var i = 0; i < collStructs.length && nCount < nLimit; i++){
				nFloored = Math.floor(nSecondsRemain / collStructs[i].seconds);
				if(nFloored > 0){
					if(sReturn.length > 0){
						if(nCount == nLimit - 1 || i == collStructs.length - 1){
							sReturn += sLastBetween;
						} else if(nCount < nLimit && i < collStructs.length){
							sReturn += sBetween;
						}
					}
					sReturn += nFloored + " " + collStructs[i].name;
					if(nFloored > 1){
						sReturn += "s";
					}
					nSecondsRemain -= nFloored * collStructs[i].seconds;
					nCount++;
				}
			}

			return sReturn;
		}
		Date.prototype.getDifference._collStructs = [
			{seconds: 60 * 60 * 24 * 365, name: "year"},
			{seconds: 60 * 60 * 24 * 30, name: "month"},
			{seconds: 60 * 60 * 24 * 7, name: "week"},
			{seconds: 60 * 60 * 24, name: "day"},
			{seconds: 60 * 60, name: "hour"},
			{seconds: 60, name: "minute"},
			{seconds: 1, name: "second"}
		];
	}
} )();

( function ( ) {

	var MAP;
	var LOCATIONS;
	var ROUTES;
	var PROJECTION;
	var CURRENT = "london";
	var OFFSET = 0;
	var PASS = true;
	var FAILED = false;

	( function ( ) {
		var fix = function ( o, n ) {
			return function ( msg, cb, noreally ) {
				if ( typeof cb != "function" ) cb = function ( ) {  }
				if ( noreally )
					cb ( o ( msg ) );
				else
					n ( msg, cb );
			}
		}
		var showMessage = function ( msg, buttons, cb ) {
			var msgBox = $('<div class="msgBox"><div class="msgContent">' + msg + '</div></div>');
			var buttonBox = $('<div class="msgButtons"></div>');
			var modalObj;
			var close = function ( rsp ) {
				$.modal.close();
				cb ( rsp );
			}
			var first = null;
			for ( button in buttons ) {
				( function ( value, response ) {
					var btn = document.createElement("input");
					btn.type = "button";
					btn.value = value;
					btn.onclick = function ( ) { close ( response ); }
					buttonBox[0].appendChild ( btn );
					if ( !first ) first = btn;
				} ) ( button, buttons[button] );
			}
			msgBox[0].appendChild ( buttonBox[0] );
			msgBox.modal ( {
				overlay : 75,
				close : false
			} );
			msgBox.css ( "paddingBottom", buttonBox[0].offsetHeight + 20 );
			msgBox[0].appendChild ( buttonBox[0] );
			if ( first ) first.focus();
		}
		window.alert = fix ( window.alert, function ( msg, cb ) {
			showMessage ( msg, { OK : true }, cb );
		} );
		window.confirm = fix ( window.confirm, function ( msg, cb ) {
			showMessage ( msg, { Yes : true, No : false }, cb );
		} );
	} )();

	window.onload = function ( ) {
		if ( $.browser.msie ) {
			var resize = ( function ( ) {
				var offset = $("td.top")[0].offsetHeight + $("td.bottom")[0].offsetHeight;
				return function ( ) {
					$("td.middle").each ( function ( ) {
						var height = document.body.offsetHeight;
						this.height = ( height - offset ) + "px";
					} );
				}
			} )();
			window.onresize = resize;
			resize();
		}
		initMap();
		initLocations();
		initClock();
		locations[CURRENT].jump();
		$("#welcome").modal ( {
			containerId : "welcomeContainer",
			close : false,
			overlay : 75,
			onShow : function ( dialog ) {
				var obj = this;
				var fn = function ( ) { obj.close (); };
				for ( var i in dialog )
					$(dialog[i]).click ( fn );
			}
		} );
	}

	function initMap ( ) {
		PROJECTION = new OpenLayers.Projection('EPSG:4326');
		//var lon = -841418;
		//var lat = 2543823;
		var lon = locations[CURRENT].geo.lon;
		var lat = locations[CURRENT].geo.lat;
		var zoom = 3;
		var mapArgs = {
			maxExtent: new OpenLayers.Bounds(-20037000, -20037000, 20037000, 20037000),
			restrictedExtent: new OpenLayers.Bounds(-15000000, -15000000, 20000000, 10000000),
			numZoomLevels: 10,
			minZoom: 2,
			maxZoom: 3,
			maxResolution: 156543.0339,
			units: 'm',
			displayProjection: PROJECTION,
			projection: 'EPSG:900913',
			controls: [ 
				new OpenLayers.Control.Navigation({zoomWheelEnabled:true}),
				new OpenLayers.Control.KeyboardDefaults()
				// new OpenLayers.Control.MousePosition({displayProjection:new OpenLayers.Projection('EPSG:900913')})
			],
			theme: null
		};
		MAP = new OpenLayers.Map ( "map", mapArgs );
		MAP.isValidZoomLevel = function ( level ) {
			var valid = ( !isNaN(level=parseInt(level)) && ( level >= this.minZoom ) && ( level <= this.maxZoom ) );
			return valid;
		}
		ROUTES = new OpenLayers.Layer.Vector ( "routes" );
		MAP.addLayer ( ROUTES );
		LOCATIONS = new OpenLayers.Layer.Markers ( "locations" );
		MAP.addLayer ( LOCATIONS );

		var layer = new OpenLayers.Layer.TMS (
			"Map",
			TILESERVER,
			{ buffer : 0, wrapDateLine : false }
		);
		layer.getURL = ( function ( ) {
			var xyzToQuadtree = function ( x, y, z ) {
				if ( z == 0 ) return "t";
				return xyzToQuadtree ( Math.floor ( x / 2 ), Math.floor ( y / 2 ), z - 1 ) +
						( ([['q','t'],['r','s']])[Math.abs(x%2)][Math.abs(y%2)] );
			}
			return function ( bounds ) {
				var res = this.map.getResolution();
				var x = Math.round ((bounds.left - this.maxExtent.left) / (res * this.tileSize.w));
				var y = Math.round ((this.maxExtent.top - bounds.top) / (res * this.tileSize.h));
				var z = this.map.getZoom();
				var serverId = ["vw", "wx", "xy", "yz"].random();
				var url = this.url.replace("{{serverId}}", serverId) + xyzToQuadtree ( x, y, z ) + ".jpg";
				return url;
			}
		} )();
		MAP.addLayer ( layer );
		MAP.setCenter ( new OpenLayers.LonLat ( lon, lat ), zoom );
		MAP.events.register ( "click", MAP, function ( ) {
			MAP.zoomTo ( -( MAP.getZoom() - 2 ) + 3 );
			/*
			if ( selected.parentNode ) {
				locations[CURRENT].focus(false);
				MAP.zoomTo ( 2 );
			}
			*/
		} );
	}

	function initLocations ( ) {
		var showInfo = ( function ( ) {
			var notes = document.getElementById("notes");
			return function ( locationId, destinations ) {
				var location = locations[locationId];
				var header = notes.getElementsByTagName("h3")[0];
				header.className = location.code;
				header.innerHTML = "<span><em>" + location.city + "</em><br>" + location.country + "</span>";
				var options = notes.getElementsByTagName("ul")[0];
				options.innerHTML = "";
				var travel = notes.getElementsByTagName("ul")[1];
				travel.innerHTML = "";
				travel.style.display = "none";
				try {
					$(".marker .tag").remove();
					for ( var i = 0; i < destinations.length; i ++ ) {
						( function ( destination ) {
							var option = document.createElement("li");
							var key = destination.name.toLowerCase().replace(/ /g,"_");
							var location = locations[key];
							var mode = destination.type.toLowerCase().replace(/ /g,"_");
							option.className = mode;
							var handle = document.createElement("span");
							handle.onclick = function ( ) {
								location.focus();
							}
							handle.className = "handle";
							handle.title = "Check " + destination.name + "'s connections";
							handle.innerHTML = "<strong>" + destination.name + "</strong><br>by " + destination.type;
							option.appendChild ( handle );
							var more = document.createElement("span");
							more.className = "more risk_" + destination.risk.toLowerCase().replace(/ /g,"_");
							more.title = destination.time + " hour";
							more.title += (destination.time == 1) ? " " : "s "; 
							more.title += destination.risk + " risk";
							more.innerHTML += "" + destination.time + " hr";
							more.innerHTML += (destination.time == 1) ? "" : "s";
							option.appendChild ( more );
							options.appendChild ( option );
							
							if ( ( locationId == CURRENT ) || ( key == CURRENT ) ) {
								travel.style.display = "";
								var option = document.createElement("li");
								option.innerHTML = ( key == CURRENT ? "Travel from " : "Travel to " ) + destination.name;
								travel.appendChild ( option );
								var destinationId = ( key == CURRENT ? locationId : key );
								var fn = function ( e ) {
									e = e || window.event;
									if ( e ) {
										e.cancelBubble = true;
										if (e.stopPropagation) e.stopPropagation();
									}
									ROUTES.destroyFeatures();
									$(".marker .tag").remove();
									var data = {
										city : locations[locationId].city,
										dest : destination.name
									}
									$.getJSON ( ENDPOINTS['travel'].url, data, function ( rsp ) {
										locations[destinationId].travelTo(rsp.time, rsp.delay, mode);
									} );
									return false;
								}
								option.onclick = fn
								locations[destinationId].showTag ( fn );
							}
						} ) ( destinations[i] );
					}
				} catch ( e ) { console.log ( e ); }
			}
		} )();
		var showRoutes = ( function ( ) {
			return function ( current, options ) {
				ROUTES.destroyFeatures();
				var features = [];
				var start = locations[current].geo;
				var styles = {
					"Low" : {
						strokeColor : "#99ff99",
						strokeWidth : 5,
						strokeOpacity : 0.75
					},
					"Medium" : {
						strokeColor : "#3366cc",
						strokeWidth : 5,
						strokeOpacity : 0.75
					},
					"High" : {
						strokeColor : "#660000",
						strokeWidth : 5,
						strokeOpacity : 0.75
					},
					"Very High" : {
						strokeColor : "#000000",
						strokeWidth : 5,
						strokeOpacity : 0.75
					}
				};
				for ( var i = 0; i < options.length; i ++ ) {
					var key = options[i].name.toLowerCase().replace(/ /g," ");
					var end = locations[key].geo;
					var geometry = new OpenLayers.Geometry.LineString ( [
						new OpenLayers.Geometry.Point ( start.lon, start.lat ),
						new OpenLayers.Geometry.Point ( end.lon, end.lat )
					] );
					var feature = new OpenLayers.Feature.Vector ( geometry, { title : options[i].risk }, styles[options[i].risk] );
					features.push ( feature );
				}
				ROUTES.addFeatures ( features );
			}
		} )();
		
		//var selected = document.createElement("div");
		//selected.className = "selected";
		var current = document.createElement("div");
		current.className = "current";
		var fetchData = function ( location, endpoint, cb, data ) {
			if ( !data ) data = {};
			var ep = ENDPOINTS[endpoint];
			if ( !ep ) return false;
			if ( ep.params ) {
				for ( var i = 0; i < ep.params.length; i ++ ) {
					data[ep.params[i]] = location[ep.params[i]];
				}
			}
			$.getJSON ( ep.url, data, cb );
			return true;
		}
		var size = new OpenLayers.Size ( 28, 28 );
		var offset = new OpenLayers.Pixel ( -(size.w/2), -(size.h/2) );
		var i = 0;
		for ( id in locations ) {
			( function ( id, i ) {
				var src = "/style/markers/" + this.code + ".png";
				var icon = new OpenLayers.Icon ( src, size, offset );
				var lonlat = new OpenLayers.LonLat ( this.geo.lon, this.geo.lat );
				var marker = new OpenLayers.Marker ( lonlat, icon );
				marker.icon.imageDiv.title = this.city + ", " + this.country;
				marker.icon.imageDiv.className = "marker";
				locations[id].focus = function ( move ) {
					if ( move !== false ) move = true;
					//marker.icon.imageDiv.insertBefore ( selected, marker.icon.imageDiv.childNodes[0] );
					if ( move ) {
						MAP.zoomTo ( 3 );
						MAP.panTo ( lonlat );
					}
					marker.icon.imageDiv.parentNode.appendChild ( marker.icon.imageDiv );
					fetchData ( locations[id], "destination", function ( rsp ) {
						showInfo ( id, rsp );
						showRoutes ( id, rsp );
					} );
				}
				locations[id].jump = function ( ) {
					CURRENT = id;
					marker.icon.imageDiv.appendChild ( current );
					this.focus();
					
					if (id == "kitzbuhel") {
						alert ( messages["success"].replace("<Codename>",CODENAME), function ( ) {
							document.getElementById("success").submit();
						} );
					}
				}
				locations[id].travelTo = function ( time, delay, mode ) {
					var message = messages[mode].random();
					var finish = ( function ( self ) {
						return function ( ) {
							OFFSET += time * 60 * 60 * 1000; // time is in hours, offset milliseconds
							setTimeout ( function ( ) {
								if ( !FAILED )
									self.jump();
							}, 500 );
						}
					} ) ( this );
					var fixMessage = function ( msg ) {
						if (delay == 1) msg = msg.replace("hours", "hour");
						return msg.replace("<Delay>", delay).replace("<Codename>", CODENAME);
					}
					if (delay) {
						if (PASS) {
							confirm ( fixMessage ( message.message + "<br>" + message.pass ), function ( confirmed ) {
								if ( confirmed )
									PASS = false;
								else
									time += delay;
								finish();
							} );
						} else {
							alert ( fixMessage ( message.message + ( message.delay ? "<br>" + message.delay : "" ) ), function ( ) {
								time += delay;
								finish();
							} );
						}
					} else {
						finish();
					}
				}
				locations[id].showTag = function ( cb ) {
					if ( $(".tag",marker.icon.imageDiv).length ) return;
					var tag = document.createElement("div");
					tag.className = "tag";
					tag.title = "Travel to " + this.city;
					tag.onclick = cb;
					marker.icon.imageDiv.insertBefore ( tag, marker.icon.imageDiv.childNodes[0] );
				}
				marker.events.register ( "click", this, function ( ) {
					this.focus();
				} );
				LOCATIONS.addMarker ( marker );
			} ).call ( locations[id], id, i++ );
		}
	}

	function initClock ( ) {
		var start = new Date();
		var end = new Date (
			start.getFullYear(), // leave the year as it is!
			start.getMonth(), // and the month
			start.getDate() + 1, // add a day
			start.getHours() + 3 + Math.floor ( Math.random() * 2 ), // and a sprinkling of hours,
			start.getMinutes() + Math.floor ( Math.random() * 20 ), // minutes,
			start.getSeconds() + Math.floor ( Math.random() * 60 ) // and seconds
		);
		var clock = document.getElementById("time");
		var tick = function ( ) {
			var now = new Date();
			now.setTime ( now.getTime() + OFFSET );
			if ( now >= end ) {
				// TODO if diff goes negative you're out of time
				fail();
			} else {
				var diff = now.getDifference ( null, null, " &amp; ", end );
				var warning = "You've got about ";
				var event = " left.";
				clock.innerHTML = warning + "<em>" + diff + "</em>" + event;
			}
		}
		var fail = function ( ) {
			FAILED = true;
			clearInterval ( interval );
			alert ( messages["failure"].random().replace("<Codename>",CODENAME), function ( ) {
				document.getElementById("failure").submit();
			} );
		}
		var interval = setInterval ( tick, 250 );
		tick();
	}

} )();
