/*
 * Copyright (c) 2011-2013 Mathias Panzenböck
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

"use strict";

if (typeof(BrowserPonies) !== "object") {

// Shims:
(function () {
	var shim = function (obj, shims) {
		for (var name in shims) {
			if (!(name in obj)) {
				obj[name] = shims[name];
			}
		}
	};

	shim(String.prototype, {
		trim: function () {
			return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
		},
		trimLeft: function () {
			return this.replace(/^\s\s*/, '');
		},
		trimRight: function () {
			return this.replace(/\s\s*$/, '');
		}
	});

	shim(Array, {
		isArray: function (object) {
			return Object.prototype.toString.call(object) === '[object Array]';
		}
	});

	shim(Array.prototype, {
		indexOf: function (searchElement, fromIndex) {
			if (!fromIndex || fromIndex < 0) fromIndex = 0;
			for (; fromIndex < this.length; ++ fromIndex) {
				if (this[fromIndex] === searchElement) {
					return fromIndex;
				}
			}
			return -1;
		}
	});

	shim(Function.prototype, {
		bind: function (self) {
			var funct   = this;
			var partial = Array.prototype.slice.call(arguments,1);
			return function () {
				return funct.apply(self,partial.concat(Array.prototype.slice.call(arguments)));
			};
		}
	});

	shim(Date, {
		now: function () {
			return new Date().getTime();
		}
	});

	// dummy console object to prevent crashes on forgotten debug messages:
	if (typeof(console) === "undefined")
		shim(window, {console: {}});
	shim(window.console, {log: function () {}});
	shim(window.console, {
		info:  window.console.log,
		warn:  window.console.log,
		error: window.console.log,
		trace: window.console.log,
		dir:   window.console.log
	});
})();

var BrowserPonies = (function () {
	var BaseZIndex = 9000000;
	var observe = document.addEventListener ?
		function (element, event, handler) {
			element.addEventListener(event, handler, false);
		} :
		function (element, event, handler) {
			var wrapper = '_eventHandlingWrapper' in handler ?
				handler._eventHandlingWrapper :
				(handler._eventHandlingWrapper = function () {
					var event = window.event;
					if (!('stopPropagation' in event)) {
						event.stopPropagation = function () {
							this.cancelBubble = true;
						};
					}
					if (!('preventDefault' in event)) {
						event.preventDefault = function() {
							this.returnValue = false;
						};
					}
					if (!('target' in event)) {
						event.target = event.srcElement;
					}
					return handler.call(this,event);
				});
			element.attachEvent('on'+event, wrapper);
		};

	var stopObserving = document.removeEventListener ?
		function (element, event, handler) {
			element.removeEventListener(event, handler, false);
		} :
		function (element, event, handler) {
			if ('_eventHandlingWrapper' in handler) {
				element.detachEvent('on'+event, handler._eventHandlingWrapper);
			}
		};

	var documentHidden = function () {
		var names = ['hidden', 'webkitHidden', 'mozHidden', 'msHidden'];
		for (var i = 0; i < names.length; ++ i) {
			var name = names[i];
			if (name in document) {
				return document[name];
			}
		}
		return false;
	};

	var visibilitychange = function (event) {
		if (timer !== null) {
			if (documentHidden()) {
				clearTimeout(timer);
			}
			else {
				lastTime = Date.now();
				tick();
			}
		}
	};

	if (typeof(document.hidden) !== 'undefined') {
		observe(document, 'visibilitychange', visibilitychange);
	}
	else if (typeof(document.webkitHidden) !== 'undefined') {
		observe(document, 'webkitvisibilitychange', visibilitychange);
	}
	else if (typeof(document.mozHidden) !== 'undefined') {
		observe(document, 'mozvisibilitychange', visibilitychange);
	}
	else if (typeof(document.msHidden) !== 'undefined') {
		observe(document, 'msvisibilitychange', visibilitychange);
	}

	var windowSize = 'innerWidth' in window ?
		function () {
			return {
				width:  window.innerWidth,
				height: window.innerHeight
			};
		} :
		function () {
			return {
				width:  document.documentElement.clientWidth,
				height: document.documentElement.clientHeight
			};
		};

	var padd = function (s,fill,padding,right) {
		if (s.length >= fill) {
			return s;
		}
		padding = new Array(fill-s.length+1).join(padding);
		return right ? (padding + s) : (s + padding);
	};

	var format = function (fmt) {
		var s = '';
		var argind = 1;
		while (fmt) {
			var m = /^([^%]*)%(-)?(0)?(\d+)?(?:\.(\d+)?)?([dfesj%])(.*)$/.exec(fmt);
			if (!m) {
				s += fmt;
				break;
			}
			s += m[1];
			fmt = m[7];

			var right   = m[2] !== '-';
			var fill    = m[4] ? parseInt(m[4]) : 0;
			var decimal = m[5] ? parseInt(m[5]) : 6;
			var padding = right ? (m[3] || ' ') : ' ';

			switch (m[6]) {
				case 'd': s += padd(parseInt(arguments[argind++]).toFixed(0),fill,padding,right); break;
				case 'f': s += padd(Number(arguments[argind++]).toFixed(decimal),fill,padding,right); break;
				case 'e': s += padd(Number(arguments[argind++]).toExponential(decimal),fill,padding,right); break;
				case 's': s += padd(String(arguments[argind++]),fill,' ',right); break;
				case 'j': s += padd(JSON.stringify(arguments[argind++]),fill,' ',right); break;
				case '%': s += padd('%',fill,' ',right);
			}
		}
		return s;
	};
	
	var extend = function (dest, src) {
		for (var name in src) {
			dest[name] = src[name];
		}
		return dest;
	};

	var partial = function (fn) {
		var args = Array.prototype.slice.call(arguments,1);
		return function () {
			return fn.apply(this,args.concat(Array.prototype.slice.call(arguments)));
		};
	};

	var URL = function URL (url) {
		var absurl = URL.abs(url);
		var match = URL.FILE_REGEX.exec(absurl);
		if (!match) match = URL.NET_REGEX.exec(absurl);
		if (!match) {
			throw new URIError("Illegal URL: "+url);
		}
		this.protocol = match[1].toLowerCase();
		this.username = match[2];
		this.password = match[3];
		this.hostname = match[4];
		this.port     = match[5];
		this.pathname = match[6] || "/";
		this.search   = match[7] || "";
		this.hash     = match[8] || "";

		if (!this.port) {
			this.port = URL.DEFAULT_PORTS[this.protocol];
		}
	
		if (this.port && URL.DEFAULT_PORTS[this.protocol] !== this.port) {
			this.host = this.hostname+':'+this.port;
		}
		else {
			this.host = this.hostname;
		}
	};

	URL.prototype = {
		toString: function () {
			return this.protocol+'//'+
				(this.username || this.password ?
					(this.username || 'anonymous')+(this.password ? ':'+this.password : '')+'@' : '')+
				this.hostname+(this.port && R4.URL.DEFAULT_PORTS[this.protocol] !== this.port ? ':'+this.port : '')+
				this.pathname+this.search+this.hash;
		}
	};

	extend(URL, {
		FILE_REGEX: /^(file:)\/\/()()()()([^#\?]*)(\?[^#]*)?(#.*)?$/i,
		NET_REGEX:  /^([a-z][-_a-z0-9]*:)\/\/(?:([^:@\/]*)(?::([^:@\/]*))?@)?([^:@\/]*)(?::(\d+))?(?:(\/[^#\?]*)(\?[^#]*)?(#.*)?)?$/i,
		DEFAULT_PORTS: {
			"http:":   "80",
			"https:": "443",
			"ftp:":    "21",
			"ftps:":  "990",
			"file:":     ""
		},
		abs: function (url, baseurl) {
			if (!baseurl) baseurl = window.location;
			if (url.slice(0,2) === '//') {
				return baseurl.protocol+url;
			}
			else if (url[0] === '/') {
				return baseurl.protocol+'//'+baseurl.host+url;
			}
			else if (url[0] === '#') {
				return baseurl.protocol+'//'+baseurl.host+baseurl.pathname+baseurl.search+url;
			}
			else if (url[0] === '?') {
				return baseurl.protocol+'//'+baseurl.host+baseurl.pathname+url;
			}
			else if ((/^[a-z][-_a-z0-9]*:/i).test(url)) {
				return url;
			}
			else {
				var path = baseurl.pathname.split('/');
				path.pop();
				if (path.length === 0) {
					path.push("");
				}
				path.push(url);
				return baseurl.protocol+'//'+baseurl.host+path.join("/");
			}
		},
		join: function (baseurl) {
			for (var i = 0; i < arguments.length; ++ i) {
				var url = arguments[i];
				
				if ((/^[a-z][-_a-z0-9]*:/i).test(url)) {
					baseurl = url;
				}
				else {
					baseurl = new URL(baseurl);
					if (url.slice(0,2) === '//') {
						baseurl = baseurl.protocol+url;
					}
					else if (url[0] === '/') {
						baseurl = baseurl.protocol+'//'+baseurl.host+url;
					}
					else if (url[0] === '#') {
						baseurl = baseurl.protocol+'//'+baseurl.host+baseurl.pathname+baseurl.search+url;
					}
					else if (url[0] === '?') {
						baseurl = baseurl.protocol+'//'+baseurl.host+baseurl.pathname+url;
					}
					else {
						baseurl = baseurl.protocol+'//'+baseurl.host+baseurl.pathname+url;
					}
				}
			}
			return URL.fix(baseurl);
		},
		fix: function (url) {
			return url.replace(/^https?:\/\/web\d?\.student\.tuwien\.ac\.at\/~e0427417\/browser-ponies\//,"https://panzi.github.com/Browser-Ponies/");
		}
	});

	var Opera = Object.prototype.toString.call(window.opera) === '[object Opera]';
	var IE, IEVersion;
	(function () {
		var m = (/MSIE ([0-9]{1,}[\.0-9]{0,})/).exec(navigator.userAgent);
		IE = !!m;
		if (IE) {
			IEVersion = m[1].split(".");
			for (var i = 0; i < IEVersion.length; ++ i) {
				IEVersion[i] = parseInt(IEVersion[i], 10);
			}
		}
	})();
	var Gecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1;
	var HasAudio = typeof(Audio) !== "undefined";
	var add = function (element, arg) {
		if (!arg) return;
		if (typeof(arg) === "string") {
			element.appendChild(document.createTextNode(arg));
		}
		else if (Array.isArray(arg)) {
			for (var i = 0, n = arg.length; i < n; ++ i) {
				add(element, arg[i]);
			}
		}
		else if (arg.nodeType === 1 || arg.nodeType === 3) {
			element.appendChild(arg);
		}
		else {
			for (var attr in arg) {
				var value = arg[attr];
				if (attr === "class" || attr === "className") {
					element.className = String(value);
				}
				else if (attr === "for" || attr === "htmlFor") {
					element.htmlFor = String(value);
				}
				else if (/^on/.test(attr)) {
					if (typeof(value) !== "function") {
						throw new Error("Event listeners must be a function.");
					}
					observe(element, attr.replace(/^on/,""), value);
				}
				else if (attr === 'style') {
					if (typeof(value) === "object") {
						for (var name in value) {
							var cssValue = value[name];
							if (name === 'float') {
								element.style.cssFloat   = cssValue;
								element.style.styleFloat = cssValue;
							}
							else if (name === 'opacity') {
								setOpacity(element, Number(cssValue));
							}
							else {
								try {
									element.style[name] = cssValue;
								}
								catch (e) {
									console.error(name+'='+cssValue+' '+e.toString());
								}
							}
						}
					}
					else {
						element.style.cssText += ";"+value;
					}
				}
				else if (attr === 'value' && element.nodeName === 'TEXTAREA') {
					element.value = value;
				}
				else if (value === true) {
					element.setAttribute(attr,attr);
				}
				else if (value === false) {
					element.removeAttribute(attr);
				}
				else {
					element.setAttribute(attr,String(value));
				}
			}
		}
	};

	var setOpacity = IE && IEVersion[0] < 10 ?
		function (element, opacity) {
			try {
				element.style.filter = element.style.filter.replace(/\balpha\([^\)]*\)/gi,'') +
					'alpha(opacity='+(Number(opacity)*100)+')';
			}
			catch (e) {}
			element.style.opacity = opacity;
		} :
		function (element, opacity) {
			element.style.opacity = opacity;
		};

	var tag = function (name) {
		var element = document.createElement(name);
		for (var i = 1, n = arguments.length; i < n; ++ i) {
			add(element, arguments[i]);
		}
		return element;
	};

	var has = function (obj, name) {
		return Object.prototype.hasOwnProperty.call(obj, name);
	};

	var removeAll = function (array, item) {
		for (var i = 0; i < array.length;) {
			if (array[i] === item) {
				array.splice(i,1);
			}
			else {
				++ i;
			}
		}
	};
	
	var dataUrl = function (mimeType, data) {
		return 'data:'+mimeType+';base64,'+Base64.encode(data);
	};

	var escapeXml = function (s) {
		return s.replace(/&/g, '&amp;').replace(
			/</g, '&lt;').replace(/>/g, '&gt;').replace(
			/"/g, '&quot;').replace(/'/g, '&apos;');
	};
	
	// inspired by:
	// http://farhadi.ir/posts/utf8-in-javascript-with-a-new-trick
	var Base64 = {
		encode: function (input) {
			return btoa(unescape(encodeURIComponent(input)));
		},
		decode: function (input) {
			return decodeURIComponent(escape(atob(input)));
		}
	};

	var PonyINI = {
		parse: function (text) {
			var lines = text.split(/\r?\n/);
			var rows = [];
			for (var i = 0, n = lines.length; i < n; ++ i) {
				var line = lines[i].trim();
				if (line.length === 0 || line.charAt(0) === "'")
					continue;
				var row = [];
				line = this.parseLine(line,row);
				if (line.length !== 0) {
					console.error("trailing text:",line);
				}
				rows.push(row);
			}
			return rows;
		},
		parseLine: function (line,row) {
			var pos;
			while ((line = line.trimLeft()).length > 0) {
				var ch = line.charAt(0);
				switch (ch) {
					case '"':
						line = line.slice(1);
						pos = line.search('"');
						if (pos < 0) pos = line.length;
						row.push(line.slice(0,pos));
						line = line.slice(pos);
						if (line.length > 0) {
							ch = line.charAt(0);
							if (ch === '"') {
								line = line.slice(1).trimLeft();
								ch = line.charAt(0);
							}
							if (line.length > 0) {
								if (ch === ',') {
									line = line.slice(1);
								}
								else if (ch !== '}') {
									console.error("data after quoted string:",line);
								}
							}
						}
						else {
							console.error("unterminated quoted string");
						}
						break;

					case ',':
						line = line.slice(1);
						row.push("");
						break;

					case '{':
						var nested = [];
						row.push(nested);
						line = this.parseLine(line.slice(1),nested).trimLeft();
						if (line.length > 0) {
							ch = line.charAt(0);
							if (ch !== '}') {
								console.error("data after list:",line);
							}
							else {
								line = line.slice(1).trimLeft();
								ch = line.charAt(0);
							}

							if (ch === ',') {
								line = line.slice(1);
							}
						}
						else {
							console.error("unterminated list");
						}
						break;

					case '}':
					case '\n':
						return line;

					default:
						pos = line.search(/[,}]/);
						if (pos < 0) pos = line.length;
						row.push(line.slice(0,pos).trim());
						line = line.slice(pos);
						if (line.length > 0) {
							ch = line.charAt(0);
							if (ch === ',') {
								line = line.slice(1);
							}
							else if (ch !== '}') {
								console.error("syntax error:",line);
							}
						}
				}
			}
			return line;
		}
	};

	var parseBoolean = function (value) {
		var s = value.trim().toLowerCase();
		if (s === "true") return true;
		else if (s === "false") return false;
		else throw new Error("illegal boolean value: "+value);
	};

	var parsePoint = function (value) {
		if (typeof(value) === "string")
			value = value.split(",");
		if (value.length !== 2 || !/^\s*-?\d+\s*$/.test(value[0]) || !/^\s*-?\d+\s*$/.test(value[1])) {
			throw new Error("illegal point value: "+value.join(","));
		}
		return {x: parseInt(value[0],10), y: parseInt(value[1],10)};
	};

	var $ = function (element_or_id) {
		if (typeof(element_or_id) === "string") {
			return document.getElementById(element_or_id);
		}
		else if (element_or_id && element_or_id.nodeType === 1) {
			return element_or_id;
		}
		else {
			return null;
		}
	};

	var distance = function (p1, p2) {
		var dx = p2.x - p1.x;
		var dy = p2.y - p1.y;
		return Math.sqrt(dx*dx + dy*dy);
	};

	var randomSelect = function (list) {
		return list[Math.floor(list.length * Math.random())];
	};

	var Movements = {
		Left:      0,
		Right:     1,
		Up:        2,
		Down:      3,
		UpLeft:    4,
		UpRight:   5,
		DownLeft:  6,
		DownRight: 7
	};

	var movementName = function (mov) {
		for (var name in Movements) {
			if (Movements[name] === mov) {
				return name;
			}
		}
		return "Not a Movement";
	};

	var AllowedMoves = {
		None:               0,
		HorizontalOnly:     1,
		VerticalOnly:       2,
		HorizontalVertical: 3,
		DiagonalOnly:       4,
		DiagonalHorizontal: 5,
		DiagonalVertical:   6,
		All:                7,
		MouseOver:          8,
		Sleep:              9,
		Dragged:           10
	};

	var Locations = {
		Top:           0,
		Bottom:        1,
		Left:          2,
		Right:         3,
		BottomRight:   4,
		BottomLeft:    5,
		TopRight:      6,
		TopLeft:       7,
		Center:        8,
		Any:           9,
		AnyNotCenter: 10
	};

	var AudioMimeTypes = {
		wav:  'audio/wav',
		webm: 'audio/webm',
		mpeg: 'audio/mpeg',
		mpga: 'audio/mpeg',
		mpg:  'audio/mpeg',
		mp1:  'audio/mpeg;codecs="mp1"',
		mp2:  'audio/mpeg;codecs="mp2"',
		mp3:  'audio/mpeg;codecs="mp3"',
		mp4:  'audio/mp4',
		mp4a: 'audio/mp4',
		ogg:  'audio/ogg',
		oga:  'audio/ogg',
		flac: 'audio/ogg;codecs="flac"',
		spx:  'audio/ogg;codecs="speex"'
	};

	var locationName = function (loc) {
		for (var name in Locations) {
			if (Locations[name] === loc) {
				return name;
			}
		}
		return "Not a Location";
	};

	var Interaction = function Interaction (interaction) {
		this.name        = interaction.name;
		this.probability = interaction.probability;
		this.proximity   = interaction.proximity === "default" ? 640 : interaction.proximity;
		this.activate    = interaction.activate;
		this.delay       = interaction.delay;
		this.targets     = [];
		this.behaviors   = [];

		for (var i = 0, n = interaction.behaviors.length; i < n; ++ i) {
			this.behaviors.push(interaction.behaviors[i].toLowerCase());
		}

		for (var i = 0, n = interaction.targets.length; i < n; ++ i) {
			var name = interaction.targets[i].toLowerCase();
			if (!has(ponies, name)) {
				console.warn("Interaction "+this.name+" of pony "+interaction.pony+
					" references non-existing pony "+name);
			}
			else {
				var pony = ponies[name];
				for (var j = 0; j < this.behaviors.length;) {
					var behavior = this.behaviors[j];
					if (has(pony.behaviors_by_name, behavior)) {
						 ++ j;
					}
					else {
						this.behaviors.splice(j, 1);
					}
				}
				this.targets.push(pony);
			}
		}
	};

	Interaction.prototype = {
		reachableTargets: function (pos) {
			var targets = [];
			var n = this.targets.length;
			if (n === 0)
				return targets;
			for (var i = 0; i < n; ++ i) {
				var pony = this.targets[i];
				var instance = null;
				var instance_dist = Infinity;
				for (var j = 0, m = pony.instances.length; j < m; ++ j) {
					var inst = pony.instances[j];
					var dist = distance(pos, inst.position());
					if (dist <= this.proximity && dist < instance_dist) {
						instance = inst;
						instance_dist = dist;
					}
				}
				if (instance) {
					targets.push([instance_dist,instance]);
				}
				else if (this.activate === "all") {
					return null;
				}
			}
			if (targets.length === 0) {
				return null;
			}
			if (this.activate === "one") {
				targets.sort(function (lhs,rhs) {
					return lhs[0] - rhs[0];
				});
				return [targets[0][1]];
			}
			else {
				for (var i = 0; i < targets.length; ++ i) {
					targets[i] = targets[i][1];
				}
			}
			return targets;
		}
	};

	var Behavior = function Behavior (baseurl, behavior) {
		extend(this, behavior);

		if (!this.name || this.name.toLowerCase() === 'none') {
			throw new Error(baseurl+': illegal behavior name '+this.name);
		}
		
		if (this.follow) this.follow = this.follow.toLowerCase();
		this.movement = null;
		var movement  = behavior.movement.replace(/[-_\s]/g,'').toLowerCase();

		for (var name in AllowedMoves) {
			if (name.toLowerCase() === movement) {
				this.movement = AllowedMoves[name];
				break;
			}
		}

		if (this.movement === null) {
			throw new Error(baseurl+": illegal movement "+behavior.movement+" for behavior "+behavior.name);
		}

		this.rightsize = {width: 0, height: 0};
		if (behavior.rightimage) {
			this.rightimage = URL.join(baseurl, behavior.rightimage);
		}
		
		this.leftsize = {width: 0, height: 0};
		if (behavior.leftimage) {
			this.leftimage = URL.join(baseurl, behavior.leftimage);
		}

		// XXX: bugfix for ini files: interprete (0, 0) as missing
		if (!this.rightcenter || (this.rightcenter.x === 0 && this.rightcenter.y === 0)) {
			this.rightcenter = {x: 0, y: 0, missing: true};
		}
		
		if (!this.leftcenter || (this.leftcenter.x === 0 && this.leftcenter.y === 0)) {
			this.leftcenter = {x: 0, y: 0, missing: true};
		}

		this.effects         = [];
		this.effects_by_name = {};
		if ('effects' in behavior) {
			for (var i = 0, n = behavior.effects.length; i < n; ++ i) {
				var effect = new Effect(baseurl, behavior.effects[i]);
				this.effects_by_name[effect.name.toLowerCase()] = effect;
				this.effects.push(effect);	
			}
		}
	};

	Behavior.prototype = {
		deref: function (property, pony) {
			var name = this[property];
			var lower_name = (name||'').toLowerCase();
			if (name && lower_name !== 'none') {
				if (has(pony.behaviors_by_name, lower_name)) {
					this[property] = pony.behaviors_by_name[lower_name];
				}
				else {
					console.warn(format("%s: Behavior %s of pony %s references non-existing behavior %s.",
						pony.baseurl, this.name, pony.name, name));
					delete this[property];
				}
			}
			else {
				delete this[property];
			}
		},
		preload: function () {
			for (var i = 0, n = this.effects.length; i < n; ++ i) {
				this.effects[i].preload();
			}

			if (this.rightimage) {
				preloadImage(this.rightimage, function (image) {
					this.rightsize.width  = image.width;
					this.rightsize.height = image.height;
					if (this.rightcenter.missing) {
						this.rightcenter = {
							x: Math.round(image.width  * 0.5),
							y: Math.round(image.height * 0.5)
						};
					}
				}.bind(this));
			}
			
			if (this.leftimage) {
				preloadImage(this.leftimage, function (image) {
					this.leftsize.width  = image.width;
					this.leftsize.height = image.height;
					if (this.leftcenter.missing) {
						this.leftcenter = {
							x: Math.round(image.width  * 0.5),
							y: Math.round(image.height * 0.5)
						};
					}
				}.bind(this));
			}
		},
		isMoving: function () {
			if (this.follow || this.x || this.x) return true;
			switch (this.movement) {
				case AllowedMoves.None:
				case AllowedMoves.MouseOver:
				case AllowedMoves.Sleep:
					return false;
				default:
					return true;
			}
		}
	};

	var parseLocation = function (value) {
		var loc = value.replace(/[-_\s]/g,'').toLowerCase();
		for (var name in Locations) {
			if (name.toLowerCase() === loc) {
				return Locations[name];
			}
		}
		throw new Error('illegal location: '+value);
	};
	
	var Effect = function Effect (baseurl, effect) {
		extend(this, effect);
		this.name = effect.name.toLowerCase();

		var locs = ['rightloc','leftloc','rightcenter','leftcenter'];
		for (var i = 0; i < locs.length; ++ i) {
			var name = locs[i];
			if (name in effect) {
				this[name] = parseLocation(effect[name]);
			}
		}

		this.rightsize = {width: 0, height: 0};
		if (effect.rightimage) {
			this.rightimage = URL.join(baseurl, effect.rightimage);
		}
		this.rightcenter_point = {x: 0, y: 0};
		
		this.leftsize = {width: 0, height: 0};
		if (effect.leftimage) {
			this.leftimage = URL.join(baseurl, effect.leftimage);
		}
		this.leftcenter_point = {x: 0, y: 0};
	};

	Effect.prototype = {
		preload: function () {
			if (this.rightimage) {
				preloadImage(this.rightimage, function (image) {
					this.rightsize.width  = image.width;
					this.rightsize.height = image.height;
					this.rightcenter_point = {
						x: Math.round(image.width  * 0.5),
						y: Math.round(image.height * 0.5)
					};
				}.bind(this));
			}
			
			if (this.leftimage) {
				preloadImage(this.leftimage, function (image) {
					this.leftsize.width  = image.width;
					this.leftsize.height = image.height;
					this.leftcenter_point = {
						x: Math.round(image.width  * 0.5),
						y: Math.round(image.height * 0.5)
					};
				}.bind(this));
			}
		}
	};

	var equalLength = function (s1, s2) {
		var n = Math.min(s1.length, s2.length);
		for (var i = 0; i < n; ++ i) {
			if (s1.charAt(i) !== s2.charAt(i)) {
				return i;
			}
		}
		return n;
	};

	var resources = {};
	var resource_count = 0;
	var resource_loaded_count = 0;
	var onload_callbacks = [];
	var onprogress_callbacks = [];

	var loadImage = function (loader,url,observer) {
		var image = loader.object = new Image();
		observe(image, 'load',  partial(observer,true));
		observe(image, 'error', partial(observer,false));
		observe(image, 'abort', partial(observer,false));
		image.src = url;
	};

	var createAudio = function (urls) {
		var audio = new Audio();
		
		if (typeof(urls) === "string") {
			audio.src = urls;
		}
		else {
			for (var type in urls) {
				var source = tag('source', {src: urls[type]});

				if (type !== "audio/x-unknown") source.type = type;

				audio.appendChild(source);
			}
		}
	
		return audio;
	};
	
	var loadAudio = function (urls) {
		return function (loader,id,observer) {
			var audio = loader.object = createAudio(urls);
			observe(audio, 'loadeddata', partial(observer,true));
			observe(audio, 'error', partial(observer,false));
			observe(audio, 'abort', partial(observer,false));
			audio.preload = 'auto';
		};
	};
	
	var preloadImage = function (url,callback) {
		preload(loadImage,url,callback);
	};
	
	var preloadAudio = function (urls,callback) {
		var fakeurl;
		if (typeof(urls) === "string") {
			fakeurl = urls;
		}
		else {
			var list = [];
			for (var type in urls) {
				list.push(urls[type]);
			}
			if (list.length === 0) {
				throw new Error("no audio url to preload");
			}
			else if (list.length === 1) {
				fakeurl = list[0];
			}
			else {
				var common = list[0];
				for (var i = 1; i < list.length; ++ i) {
					var n = equalLength(common, list[i]);
					if (n !== common.length) {
						common = common.slice(0,n);
					}
				}
				for (var i = 0; i < list.length; ++ i) {
					list[i] = list[i].slice(common.length);
				}
			
				list.sort();
				fakeurl = common+'{'+list.join('|')+'}';
			}
		}

		preload(loadAudio(urls),fakeurl,callback);
	};

	var preload = function (load,url,callback) {
		if (has(resources,url)) {
			if (callback) {
				var loader = resources[url];
				if (loader.loaded) {
					callback(loader.object);
				}
				else {
					loader.callbacks.push(callback);
				}
			}
		}
		else {
			++ resource_count;
			var loader = resources[url] = {
				loaded: false,
				callbacks: callback ? [callback] : []
			};
			
			load(loader, url, function (success) {
				if (loader.loaded) {
					console.error('resource loaded twice: '+url);
					return;
				}
				loader.loaded = true;
				++ resource_loaded_count;
				if (success) {
					console.log(format('%3.0f%% %d of %d loaded: %s',
						resource_loaded_count * 100 / resource_count,
						resource_loaded_count, resource_count,
						url));
				}
				else {
					console.error(format('%3.0f%% %d of %d load error: %s',
						resource_loaded_count * 100 / resource_count,
						resource_loaded_count, resource_count,
						url));
				}
				for (var i = 0, n = onprogress_callbacks.length; i < n; ++ i) {
					onprogress_callbacks[i](resource_loaded_count, resource_count, url, success);
				}
				for (var i = 0, n = loader.callbacks.length; i < n; ++ i) {
					loader.callbacks[i](loader.object, success);
				}
				delete loader.callbacks;
				
				if (resource_loaded_count === resource_count) {
					for (var i = 0, n = onload_callbacks.length; i < n; ++ i) {
						onload_callbacks[i]();
					}
					onload_callbacks = [];
				}
			});
		}
	};
	
	preload(function (loader,url,observer) {
		if (document.body) {
			observer(true);
		}
		else {
			var loaded = false;
			var fireLoad = function () {
				if (!loaded) {
					loaded = true;
					observer(true);
				}
			};

			if (document.addEventListener) {
				// all browsers but IE implement HTML5 DOMContentLoaded
				observe(document, 'DOMContentLoaded', fireLoad);
			}
			else {
				var checkReadyState = function () {
					if (document.readyState === 'complete') {
						stopObserving(document, 'readystatechange', checkReadyState);
						fireLoad();
					}
				};

				observe(document, 'readystatechange', checkReadyState);
			}

			// fallback
			observe(window, 'load', fireLoad);
		}
	}, document.location.href);

	var onload = function (callback) {
		if (resource_loaded_count === resource_count) {
			callback();
		}
		else {
			onload_callbacks.push(callback);
		}
	};

	var onprogress = function (callback) {
		onprogress_callbacks.push(callback);
	};

	var resource_count_for_progress = 0;
	var progressbar = null;
	var insertProgressbar = function () {
		resource_count_for_progress = resource_loaded_count;
		document.body.appendChild(progressbar.container);
		centerProgressbar();
		setTimeout(function () {
			if (progressbar && !progressbar.finished) {
				progressbar.container.style.display = '';
			}
		}, 250);
		observe(window,'resize',centerProgressbar);
		stopObserving(window,'load',insertProgressbar);
	};

	var centerProgressbar = function () {
		var winsize = windowSize();
		var hide = false;
		if (progressbar.container.style.display === "none") {
			hide = true;
			progressbar.container.style.visibility = 'hidden';
			progressbar.container.style.display = '';
		}
		var width  = progressbar.container.offsetWidth;
		var height = progressbar.container.offsetHeight;
		var labelHeight = progressbar.label.offsetHeight;
		if (hide) {
			progressbar.container.style.display = 'none';
			progressbar.container.style.visibility = '';
		}
		progressbar.container.style.left = Math.round((winsize.width  - width)  * 0.5)+'px';
		progressbar.container.style.top  = Math.round((winsize.height - height) * 0.5)+'px';
		progressbar.label.style.top = Math.round((height - labelHeight) * 0.5)+'px';
	};

	onprogress(function (resource_loaded_count, resource_count, url) {
		if (showLoadProgress || progressbar) {
			if (!progressbar) {
				progressbar = {
					bar: tag('div', {style:{
						margin:            '0',
						padding:           '0',
						borderStyle:    'none',
						width:             '0',
						height:         '100%',
						background:  '#9BD6F4',
						MozBorderRadius: '5px',
						borderRadius:    '5px'
					}}),
					label: tag('div', {style:{
						position: 'absolute',
						margin:          '0',
						padding:         '0',
						borderStyle:  'none',
						top:           '0px',
						left:          '0px',
						width:        '100%',
						textAlign:  'center'
					}})
				};
				progressbar.barcontainer = tag('div', {style:{
					margin:            '0',
					padding:           '0',
					borderStyle:    'none',
					width:          '100%',
					height:         '100%',
					background:  '#D8D8D8',
					MozBorderRadius: '5px',
					borderRadius:    '5px'
				}}, progressbar.bar);
				progressbar.container = tag('div', {style:{
					position:      'fixed',
					width:         '450px',
					height:         '30px',
					background:    'white',
					padding:        '10px',
					margin:            '0',
					MozBorderRadius: '5px',
					borderRadius:    '5px',
					color:       '#294256',
					fontWeight:     'bold',
					fontSize:       '16px',
					opacity:         '0.9',
					display:        'none',
					boxShadow:    "2px 2px 12px rgba(0,0,0,0.4)",
					MozBoxShadow: "2px 2px 12px rgba(0,0,0,0.4)"
				}, onclick: function () {
					if (progressbar) {
						progressbar.container.style.display = 'none';
					}
				}}, progressbar.barcontainer, progressbar.label);
			}

			if (progressbar.container.style.display === 'none') {
				resource_count_for_progress = resource_loaded_count;
			}
			
			progressbar.finished = resource_loaded_count === resource_count;

			var loaded = resource_loaded_count - resource_count_for_progress;
			var count = resource_count - resource_count_for_progress;
			var progress = count === 0 ? 1.0 : loaded / count;
			progressbar.bar.style.width = Math.round(progress * 450)+'px';
			progressbar.label.innerHTML = format('Loading Ponies&hellip; %d%%',Math.floor(progress * 100));

			if (!progressbar.container.parentNode) {
				if (document.body) {
					insertProgressbar();
				}
				else {
					observe(window,'load',insertProgressbar);
				}
			}

			if (progressbar.finished) {
				setTimeout(function () {
					stopObserving(window,'resize',centerProgressbar);
					stopObserving(window,'load',insertProgressbar);
					if (progressbar && progressbar.container && progressbar.container.parentNode) {
						progressbar.container.parentNode.removeChild(progressbar.container);
					}
					progressbar = null;
				}, 500);
			}
		}
	});

	var Pony = function Pony (pony) {
		this.baseurl = URL.join(globalBaseUrl, pony.baseurl||"");
		if (!pony.name) {
			throw new Error('pony with following base URL has no name: '+this.baseurl);
		}
		this.name = pony.name;
		this.behaviorgroups      = pony.behaviorgroups || {};
		this.all_behaviors       = [];
		this.random_behaviors    = [];
		this.mouseover_behaviors = [];
		this.dragged_behaviors   = [];
		this.stand_behaviors     = [];
		this.behaviors_by_name   = {};
		this.speeches = [];
		this.random_speeches  = [];
		this.speeches_by_name = {};
		this.interactions = [];
		this.instances    = [];
		this.categories   = [];

		if (pony.categories) {
			for (var i = 0, n = pony.categories.length; i < n; ++ i) {
				this.categories.push(pony.categories[i].toLowerCase());
			}
		}
		
		if (pony.speeches) {
			for (var i = 0, n = pony.speeches.length; i < n; ++ i) {
				var speech = extend({},pony.speeches[i]);
				if (speech.files) {
					var count = 0;
					for (var type in speech.files) {
						speech.files[type] = URL.join(this.baseurl, speech.files[type]);
						++ count;
					}
					if (count === 0) {
						delete speech.files;
					}
				}
				if (speech.name) {
					var lowername = speech.name.toLowerCase();
					if (has(this.speeches_by_name,lowername)) {
						console.warn(format("%s: Speech name %s of pony %s is not unique.",
							this.baseurl, speech.name, pony.name));
					}
					else {
						this.speeches_by_name[lowername] = speech;
					}
				}
				if (!('skip' in speech)) {
					speech.skip = false;
				}
				if (!speech.skip) {
					this.random_speeches.push(speech);
				}
				if ('group' in speech) {
					if (speech.group !== 0 && !has(this.behaviorgroups,speech.group)) {
						console.warn(format("%s: Speech %s references unknown behavior group %d.",
							this.baseurl, speech.name, speech.group));
					}
				}
				else {
					speech.group = 0;
				}
				this.speeches.push(speech);
			}
		}

		var speakevents = ['speakstart','speakend'];
		if ('behaviors' in pony) {
			for (var i = 0, n = pony.behaviors.length; i < n; ++ i) {
				var behavior = new Behavior(this.baseurl, pony.behaviors[i]);
				var lowername = behavior.name.toLowerCase();
				if (has(this.behaviors_by_name,lowername)) {
					console.warn(format("%s: Behavior name %s of pony %s is not unique.",
						this.baseurl, behavior.name, pony.name));
				}
				else {
					// semantics like Dektop Ponies where the
					// first match is used for linked behaviors
					this.behaviors_by_name[lowername] = behavior;
				}
				for (var j = 0; j < speakevents.length; ++ j) {
					var speakevent = speakevents[j];
					var speechname = behavior[speakevent];
					if (speechname) {
						speechname = speechname.toLowerCase();
						if (has(this.speeches_by_name,speechname)) {
							behavior[speakevent] = this.speeches_by_name[speechname];
						}
						else {
							console.warn(format("%s: Behavior %s of pony %s references non-existing speech %s.",
								this.baseurl, behavior.name, pony.name, behavior[speakevent]));
							delete behavior[speakevent];
						}
					}
				}
				this.all_behaviors.push(behavior);
				if (!('skip' in behavior)) {
					behavior.skip = false;
				}
				if (!behavior.skip) this.random_behaviors.push(behavior);

				switch (behavior.movement) {
					case AllowedMoves.MouseOver:
						this.mouseover_behaviors.push(behavior);
						if (!behavior.skip) this.stand_behaviors.push(behavior);
						break;

					case AllowedMoves.Dragged:
						this.dragged_behaviors.push(behavior);
						if (!behavior.skip) this.stand_behaviors.push(behavior);
						break;

					case AllowedMoves.None:
						if (!behavior.skip) this.stand_behaviors.push(behavior);
						break;
				}
				
				if ('group' in behavior) {
					if (behavior.group !== 0 && !has(this.behaviorgroups,behavior.group)) {
						console.warn(format("%s: Behavior %s references unknown behavior group %d.",
							this.baseurl, behavior.name, behavior.group));
					}
				}
				else {
					behavior.group = 0;
				}
			}

			if (this.dragged_behaviors.length === 0 && this.mouseover_behaviors.length > 0) {
				this.dragged_behaviors = this.mouseover_behaviors.slice();
			}

			if (this.stand_behaviors.length === 0) {
				for (var i = 0, n = this.all_behaviors.length; i < n; ++ i) {
					var behavior = this.all_behaviors[i];
					if (behavior.movement === AllowedMoves.Sleep && !behavior.skip) {
						this.stand_behaviors.push(behavior);
					}
				}
			}

			if (this.stand_behaviors.length === 0) {
				console.warn(format("%s: Pony %s has no (non-skip) non-moving behavior.", this.baseurl, this.name));
			}
			else if (this.mouseover_behaviors.length === 0) {
				this.mouseover_behaviors = this.stand_behaviors.slice();
			}
			
			// dereference linked behaviors:
			for (var i = 0, n = this.all_behaviors.length; i < n; ++ i) {
				var behavior = this.all_behaviors[i];
				behavior.deref('linked',this);
				behavior.deref('stopped',this);
				behavior.deref('moving',this);
			}
		}
	};

	Pony.prototype = {
		preload: function () {
			for (var i = 0, n = this.all_behaviors.length; i < n; ++ i) {
				this.all_behaviors[i].preload();
			}
			
			if (HasAudio && audioEnabled) {
				for (var i = 0, n = this.speeches.length; i < n; ++ i) {
					var speech = this.speeches[i];
					if (speech.files) {
						preloadAudio(speech.files);
					}
				}
			}
		},
		unspawnAll: function () {
			while (this.instances.length > 0) {
				this.instances[0].unspawn();
			}
		},
		addInteraction: function (interaction) {
			interaction = new Interaction(interaction);

			if (interaction.targets.length === 0) {
				console.warn("Dropping interaction "+interaction.name+" of pony "+this.name+
					" because it has no targets.");
				return false;
			}
			
			for (var i = 0; i < interaction.behaviors.length;) {
				var behavior = interaction.behaviors[i];
				if (has(this.behaviors_by_name, behavior)) {
					 ++ i;
				}
				else {
					interaction.behaviors.splice(i, 1);
				}
			}

			if (interaction.behaviors.length === 0) {
				console.warn("Dropping interaction "+interaction.name+" of pony "+this.name+
					" because it has no common behaviors.");
				return false;
			}

			this.interactions.push(interaction);
			return true;
		}
	};

	var descendantOf = function (child, parent) {
		var node = child.parentNode;
		while (node) {
			if (node === parent) {
				return true;
			}
		}
		return false;
	};
	
	var isOffscreen = function (rect) {
		return isOutsideOf(rect, windowSize());
	};

	// rect has origin at center
	// area is only a size
	var isOutsideOf = function (rect, area) {
		var wh = rect.width  * 0.5;
		var hh = rect.height * 0.5;
		return rect.x < wh || rect.y < hh ||
			rect.x + wh > area.width || 
			rect.y + hh > area.height;
	};

	var clipToScreen = function (rect) {
		var winsize = windowSize();
		var x = rect.x;
		var y = rect.y;
		var wh = rect.width  * 0.5;
		var hh = rect.height * 0.5;

		if (x < wh) {
			x = wh;
		}
		else if (x + wh > winsize.width) {
			x = winsize.width - wh;
		}

		if (y < hh) {
			y = hh;
		}
		else if (y + hh > winsize.height) {
			y = winsize.height - hh;
		}

		return {x: Math.round(x), y: Math.round(y)};
	};

	var Instance = function Instance () {};
	Instance.prototype = {
		setTopLeftPosition: function (pos) {
			this.current_position.x = pos.x + this.current_center.x;
			this.current_position.y = pos.y + this.current_center.y;
			this.img.style.left = Math.round(pos.x)+'px';
			this.img.style.top  = Math.round(pos.y)+'px';
			var zIndex = Math.round(BaseZIndex + pos.y + this.current_size.height);
			if (this.zIndex !== zIndex) {
				this.img.style.zIndex = zIndex;
			}
		},
		setPosition: function (pos) {
			var x = this.current_position.x = pos.x;
			var y = this.current_position.y = pos.y;
			var top = Math.round(y - this.current_center.y);
			this.img.style.left = Math.round(x - this.current_center.x)+'px';
			this.img.style.top  = top+'px';
			var zIndex = Math.round(BaseZIndex + top + this.current_size.height);
			if (this.zIndex !== zIndex) {
				this.img.style.zIndex = zIndex;
			}
		},
		moveBy: function (offset) {
			this.setPosition({
				x: this.current_position.x + offset.x,
				y: this.current_position.y + offset.y
			});
		},
		clipToScreen: function () {
			this.setPosition(clipToScreen(this.rect()));
		},
		topLeftPosition: function () {
			return {
				x: this.current_position.x - this.current_center.x,
				y: this.current_position.y - this.current_center.y
			};
		},
		position: function () {
			return this.current_position;
		},
		size: function () {
			return this.current_size;
		},
		rect: function () {
			// lets abuse for speed (avoid object creation)
			var pos = this.current_position;
			pos.width  = this.current_size.width;
			pos.height = this.current_size.height;
			return pos;

//			var pos  = this.position();
//			var size = this.size();
//			return {
//				x: pos.x,
//				y: pos.y,
//				width:  size.width,
//				height: size.height
//			};
		},
		topLeftRect: function () {
			var pos  = this.topLeftPosition();
			var size = this.size();
			return {
				x: pos.x,
				y: pos.y,
				width:  size.width,
				height: size.height
			};
		},
		isOffscreen: function () {
			return isOffscreen(this.rect());
		}
	};

	var PonyInstance = function PonyInstance (pony) {
		this.pony = pony;
		this.img  = this.createImage();

		this.clear();
	};

	PonyInstance.prototype = extend(new Instance(), {
		createImage: function () {
			var touch = function(evt) {
				evt.preventDefault();
				if (evt.touches.length > 1 || (evt.type === "touchend" && evt.touches.length > 0))
				return;

				var newEvt = document.createEvent("MouseEvents");
				var type = null;
				var touch = null;
				switch (evt.type) {
					case "touchstart":
						type = "mousedown";
						touch = evt.changedTouches[0];
						break;
					case "touchmove":
						type = "mousemove";
						touch = evt.changedTouches[0];
						break;
					case "touchend":
						type = "mouseup";
						touch = evt.changedTouches[0];
						break;
				}
				newEvt.initMouseEvent(type, true, true, evt.target.ownerDocument.defaultView, 1,
					touch.screenX, touch.screenY, touch.clientX, touch.clientY,
					evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, 0, null);
				evt.target.dispatchEvent(newEvt);
			};
			return tag('img', {
				draggable: 'false',
				style: {
					position:        "fixed",
					userSelect:      "none",
					borderStyle:     "none",
					margin:          "0",
					padding:         "0",
					backgroundColor: "transparent",
					zIndex:          String(BaseZIndex)
				},
				ondragstart: function (event) {
					event.preventDefault();
				},
				ontouchstart: touch,
				ontouchmove: touch,
				ontouchend: touch,
				ondblclick: function () {
					// debug output
					var pos = this.position();
					var duration = (this.end_time - this.start_time) / 1000;
					console.log(
						format('%s does %s%s for %.2f seconds, is at %d x %d and %s. See:',
							this.pony.name, this.current_behavior.name,
							this.current_behavior === this.paint_behavior ? '' :
							' using '+this.paint_behavior.name, duration, pos.x, pos.y,
							(this.following ?
								'follows '+this.following.name() :
								format('wants to go to %d x %d',
									this.dest_position.x, this.dest_position.y))),
						this);
				}.bind(this),
				onmousedown: function (event) {
					// IE 9 supports event.buttons and handles event.button like the w3c says.
					// IE <9 does not support event.buttons but sets event.button to the value
					// event.buttons should have (which is not what the w3c says).
					if ('buttons' in event ? event.buttons & 1 : (IE ? event.button & 1 : event.button === 0)) {
						dragged = this;
						this.mouseover = true;
						// timer === null means paused/not running
						if (timer !== null) {
							this.nextBehavior(true);
						}
						event.preventDefault();
					}
				}.bind(this),
				onmouseover: function () {
					if (!this.mouseover) {
						this.mouseover = true;
						// timer === null means paused/not runnung
						if (timer !== null &&
							!this.isMouseOverOrDragging() &&
							(this.canMouseOver() || this.canDrag())) {
							this.nextBehavior(true);
						}
					}
				}.bind(this),
				onmouseout: function (event) {
					var target = event.target;
					// XXX: the img has no descendants but if it had it might still be correct in case
					//      the relatedTarget is an anchester of the img or any node that is not a child
					//      of img or img itself.
//					if (this.mouseover && (target === this.img || !descendantOf(target, this.img))) {
					if (this.mouseover) {
						this.mouseover = false;
					}
				}.bind(this)
			});
		},
		isMouseOverOrDragging: function () {
			return this.current_behavior &&
				(this.current_behavior.movement === AllowedMoves.MouseOver ||
				 this.current_behavior.movement === AllowedMoves.Dragged);
		},
		canDrag: function () {
			if (!this.current_behavior) {
				return this.pony.dragged_behaviors.length > 0;
			}
			else {
				var current_group = this.current_behavior.group;
				
				for (var i = 0, n = this.pony.dragged_behaviors.length; i < n; ++ i) {
					var behavior = this.pony.dragged_behaviors[i];
					if (behavior.group === 0 || behavior.group === current_group) {
						return true;
					}
				}

				return false;
			}
		},
		canMouseOver: function () {
			if (!this.current_behavior) {
				return this.pony.mouseover_behaviors.length > 0;
			}
			else {
				var current_group = this.current_behavior.group;
				for (var i = 0, n = this.pony.mouseover_behaviors.length; i < n; ++ i) {
					var behavior = this.pony.mouseover_behaviors[i];
					if (behavior.group === 0 || behavior.group === current_group) {
						return true;
					}
				}

				return false;
			}
		},
		name: function () {
			return this.pony.name;
		},
		unspawn: function () {
			var currentTime = Date.now();
			if (this.effects) {
				for (var i = 0, n = this.effects.length; i < n; ++ i) {
					removing.push({
						at: currentTime,
						element: this.effects[i].img
					});
				}
			}
			removing.push({
				at: currentTime,
				element: this.img
			});
			removeAll(this.pony.instances,this);
			removeAll(instances,this);
		},
		clear: function () {
			if (this.effects) {
				for (var i = 0, n = this.effects.length; i < n; ++ i) {
					this.effects[i].clear();
				}
			}
			if (this.img.parentNode) {
				this.img.parentNode.removeChild(this.img);
			}
			this.mouseover           = false;
			this.start_time          = null;
			this.end_time            = null;
			this.current_interaction = null;
			this.interaction_targets = null;
			this.current_imgurl      = null;
			this.interaction_wait    = 0;
			this.current_position    = {x: 0, y: 0};
			this.dest_position       = {x: 0, y: 0};
			this.current_size        = {width: 0, height: 0};
			this.current_center      = {x: 0, y: 0};
			this.zIndex              = BaseZIndex;
			this.current_behavior    = null;
			this.paint_behavior      = null;
			this.facing_right        = true;
			this.end_at_dest         = false;
			this.effects             = [];
			this.repeating           = [];
		},
		interact: function (currentTime,interaction,targets) {
			var pony, behavior = randomSelect(interaction.behaviors);
			this.behave(this.pony.behaviors_by_name[behavior]);
			for (var i = 0, n = targets.length; i < n; ++ i) {
				pony = targets[i];
				pony.behave(pony.pony.behaviors_by_name[behavior]);
				pony.current_interaction = interaction;
			}
			this.current_interaction = interaction;
			this.interaction_targets = targets;
		},
		speak: function (currentTime,speech) {
			if (dontSpeak) return;
			if (speech.text) {
				var duration = Math.max(speech.text.length * 150, 1000);
				var remove = {at: currentTime + duration};
				var text = tag('div',{
					ondblclick: function () {
						remove.at = Date.now();
					},
					style: {
						fontSize:        "14px",
						color:        "#294256",
						background: IE ? "white" : "rgba(255,255,255,0.8)",
						position:       "fixed",
						visibility:    "hidden",
						margin:             "0",
						padding:          "4px",
						maxWidth:       "250px",
						textAlign:     "center",
						borderRadius:    "10px",
						MozBorderRadius: "10px",
						width:           'auto',
						height:          'auto',
						boxShadow:    "2px 2px 12px rgba(0,0,0,0.4)",
						MozBoxShadow: "2px 2px 12px rgba(0,0,0,0.4)",
						zIndex: String(BaseZIndex + 9000)
					}}, speech.text);
				remove.element = text;
				var rect = this.topLeftRect();
				getOverlay().appendChild(text);
				var x = Math.round(rect.x + rect.width * 0.5 - text.offsetWidth * 0.5);
				var y = rect.y + rect.height;
				text.style.left = x+'px';
				text.style.top  = y+'px';
				text.style.visibility = '';
				removing.push(remove);
				text = null;
			}
			if (HasAudio && audioEnabled && speech.files) {
				var audio = createAudio(speech.files);
				audio.volume = volume;
				audio.play();
			}
		},
		update: function (currentTime, passedTime, winsize) {
			var curr = this.rect();
			var dest = null;
			var dist;
			if (this.following) {
				if (this.following.img.parentNode) {
					dest   = this.dest_position;
					dest.x = this.following.current_position.x;

					if (this.following.facing_right) {
						dest.x += this.current_behavior.x - this.following.paint_behavior.rightcenter.x;
//						dest.x += this.current_behavior.x - this.following.paint_behavior.rightcenter.x + 40;
//						dest.x += -this.following.paint_behavior.rightcenter.x + 50;
					}
					else {
						dest.x += -this.current_behavior.x + this.following.paint_behavior.leftcenter.x;
//						dest.x += -this.current_behavior.x + this.following.paint_behavior.leftcenter.x - 20;
//						dest.x += this.following.paint_behavior.leftcenter.x - 30;
					}
					dest.y = this.following.current_position.y + this.current_behavior.y;
					dist = distance(curr, dest);
					if (!this.current_behavior.x && !this.current_behavior.y &&
						dist <= curr.width * 0.5) {
						dest = null;
					}
				}
				else {
					this.following = null;
				}
			}
			else {
				dest = this.dest_position;
				if (dest) dist = distance(curr, dest);
			}

			var pos;
			if (dest) {
				var dx = dest.x - curr.x;
				var dy = dest.y - curr.y;
				var tdist = this.current_behavior.speed * passedTime * 0.01 * globalSpeed;

				if (tdist >= dist) {
					pos = dest;
				}
				else {
					var scale = tdist / dist;
					pos = {
						x: Math.round(curr.x + scale * dx),
						y: Math.round(curr.y + scale * dy)
					};
				}

				if (pos.x !== dest.x) {
					this.setFacingRight(pos.x <= dest.x);
				}
				else if (this.following) {
					if (this.current_behavior.auto_select_images) {
						// TODO: mechanism for selecting behavior for current movement
					}
					else if (Math.round(tdist) === 0) {
						if (this.current_behavior.stopped) {
							this.paint_behavior = this.current_behavior.stopped;
						}
					}
					else {
						if (this.current_behavior.moving) {
							this.paint_behavior = this.current_behavior.moving;
						}
					}
					this.setFacingRight(this.following.facing_right);
				}
				this.setPosition(pos);
/*
				console.log(
					"current: "+curr.x+" x "+curr.y+
					", step: "+pos.x+" x "+pos.y+
					", dest: "+dest.x+" x "+dest.y+
					", dist: "+dist+
					", dist for passed time: "+tdist);
*/
			}
			else {
				pos = curr;
			}

			// update associated effects:
			for (var i = 0; i < this.effects.length;) {
				var effect = this.effects[i];
				if (effect.update(currentTime, passedTime, winsize)) {
					++ i;
				}
				else {
					this.effects.splice(i, 1);
					removing.push({
						element: effect.img,
						at: currentTime
					});
				}
			}
			
			// check if some effects need to be repeated:
			for (var i = 0, n = this.repeating.length; i < n; ++ i) {
				var what = this.repeating[i];
				if (what.at <= currentTime) {
					var inst = new EffectInstance(this, currentTime, what.effect);
					overlay.appendChild(inst.img);
					inst.updatePosition(currentTime, 0);
					this.effects.push(inst);
					what.at += what.effect.delay * 1000;
				}
			}
			
			if (this.interaction_wait <= currentTime &&
					this.pony.interactions.length > 0 &&
					!this.current_interaction) {
				var sumprob = 0;
				var interactions = [];
				var interaction = null;
				for (var i = 0, n = this.pony.interactions.length; i < n; ++ i) {
					interaction = this.pony.interactions[i];
					var targets = interaction.reachableTargets(curr);
					if (targets) {
						sumprob += interaction.probability;
						interactions.push([interaction, targets]);
					}
				}
				
				if (interactions.length > 0) {
					var dice = Math.random() * sumprob;
					var diceiter = 0;
					for (var i = 0, n = interactions.length; i < n; ++ i) {
						interaction = interactions[i];
						diceiter += interaction.probability;
						if (dice <= diceiter) {
							break;
						}
					}

					// The probability is meant for an execution evere 100ms,
					// but I use a configurable interaction interval.
					dice = Math.random() * (100 / interactionInterval);
					if (dice <= interaction[0].probability) {
						this.interact(currentTime,interaction[0],interaction[1]);
						return;
					}
				}

				this.interaction_wait += interactionInterval;
			}

			if (currentTime >= this.end_time ||
				(this.end_at_dest &&
				 this.dest_position.x === pos.x &&
				 this.dest_position.y === pos.y)) {
				this.nextBehavior();
				return;
			}

			if (this.following) return;

			var x1 = this.current_center.x;
			var y1 = this.current_center.y;
			var x2 = this.current_size.width  - x1;
			var y2 = this.current_size.height - y1;
			var left   = pos.x - x1;
			var right  = pos.x + x2;
			var top    = pos.y - y1;
			var bottom = pos.y + y2;

			// bounce of screen edges
			if (left <= 0) {
				if (this.dest_position.x < pos.x) {
					this.dest_position.x = Math.round(Math.max(pos.x + pos.x - this.dest_position.x, x1));
				}
			}
			else if (right >= winsize.width) {
				if (this.dest_position.x > pos.x) {
					this.dest_position.x = Math.round(Math.min(pos.x + pos.x - this.dest_position.x, winsize.width - x2));
				}
			}
			
			if (top <= 0) {
				if (this.dest_position.y < pos.y) {
					this.dest_position.y = Math.round(Math.max(pos.y + pos.y - this.dest_position.y, y1));
				}
			}
			else if (bottom >= winsize.height) {
				if (this.dest_position.y > pos.y) {
					this.dest_position.y = Math.round(Math.min(pos.y + pos.y - this.dest_position.y, winsize.height - y2));
				}
			}
		},
		getNearestInstance: function (name) {
			var nearObjects = [];
			var pos = this.position();
			var pony = ponies[name];
			
			if (!pony) {
				for (var i = 0, n = instances.length; i < n; ++ i) {
					var inst = instances[i];
					if (!this.loops(inst)) {
						for (var j = 0, m = inst.effects.length; j < m; ++ j) {
							var effect = inst.effects[j];
							if (effect.effect.name === name) {
								nearObjects.push([distance(pos, effect.position()), effect]);
							}
						}
					}
				}
			}
			else {
				for (var i = 0, n = pony.instances.length; i < n; ++ i) {
					var inst = pony.instances[i];
					if (!this.loops(inst)) {
						nearObjects.push([distance(pos, inst.position()), inst]);
					}
				}
			}
			
			if (nearObjects.length === 0) {
				return null;
			}
			nearObjects.sort(function (lhs,rhs) { return lhs[0] - rhs[0]; });
			return nearObjects[0][1];
		},
		nextBehavior: function (breaklink) {
			var offscreen = this.isOffscreen();
			if (!breaklink && this.current_behavior && this.current_behavior.linked) {
				this.behave(this.current_behavior.linked, offscreen);
			}
			else {				
				if (this.current_interaction) {
					var currentTime = Date.now();
					this.interaction_wait = currentTime + this.current_interaction.delay * 1000;
					if (this.interaction_targets) {
						// XXX: should I even do this or should I just let the targets do it?
						//      they do it anyway, because current_interaction is also set for them
						//      if it wouldn't be set, they could break out of interactions
						for (var i = 0, n = this.interaction_targets.length; i < n; ++ i) {
							this.interaction_targets[i].interaction_wait = this.interaction_wait;
						}
						this.interaction_targets = null;
					}
					this.current_interaction = null;
				}

				this.behave(this.randomBehavior(offscreen), offscreen);
			}
		},
		setFacingRight: Gecko ?
		function (value) {
			this.facing_right = value;
			var newimg;
			if (value) {
				newimg = this.paint_behavior.rightimage;
				this.current_size   = this.paint_behavior.rightsize;
				this.current_center = this.paint_behavior.rightcenter;
			}
			else {
				newimg = this.paint_behavior.leftimage;
				this.current_size   = this.paint_behavior.leftsize;
				this.current_center = this.paint_behavior.leftcenter;
			}
			if (newimg !== this.current_imgurl) {
				// gif animation bug workaround
				var img = this.createImage();
				img.style.left   = this.img.style.left;
				img.style.top    = this.img.style.top;
				img.style.zIndex = this.img.style.zIndex;
				img.src = this.current_imgurl = newimg;
				this.img.parentNode.replaceChild(img, this.img);
				this.img = img;
			}
		} :
		function (value) {
			this.facing_right = value;
			var newimg;
			if (value) {
				newimg = this.paint_behavior.rightimage;
				this.current_size   = this.paint_behavior.rightsize;
				this.current_center = this.paint_behavior.rightcenter;
			}
			else {
				newimg = this.paint_behavior.leftimage;
				this.current_size   = this.paint_behavior.leftsize;
				this.current_center = this.paint_behavior.leftcenter;
			}
			if (newimg !== this.current_imgurl) {
				this.img.src = this.current_imgurl = newimg;
			}
		},
		behave: function (behavior, moveIntoScreen) {
			this.start_time = Date.now();
			var duration = (behavior.minduration +
				(behavior.maxduration - behavior.minduration) * Math.random());
			this.end_time = this.start_time + duration * 1000;
			var previous_behavior = this.current_behavior;
			this.current_behavior = this.paint_behavior = behavior;

			var neweffects = [];
			for (var i = 0, n = this.effects.length; i < n; ++ i) {
				var inst = this.effects[i];
				if (inst.effect.duration) {
					neweffects.push(inst);
				}
				else {
					removing.push({
						element: inst.img,
						at: this.start_time
					});
				}
			}
			
			// get new image + size
			if (this.facing_right) {
				this.current_size   = this.paint_behavior.rightsize;
				this.current_center = this.paint_behavior.rightcenter;
			}
			else {
				this.current_size   = this.paint_behavior.leftsize;
				this.current_center = this.paint_behavior.leftcenter;
			}
			
			var spoke = false;
			if (previous_behavior && previous_behavior.speakend) {
				this.speak(this.start_time, previous_behavior.speakend);
				spoke = true;
			}

			this.following = null;
			if (behavior.follow) {
				this.following = this.getNearestInstance(behavior.follow);
			}

			if (behavior.speakstart) {
				this.speak(this.start_time, behavior.speakstart);
			}
			else if (!spoke &&
				!this.following &&
				!this.current_interaction) {
				this.speakRandom(this.start_time, speakProbability);
			}
			
			var pos  = this.position();
			var size = this.size();
			var winsize = windowSize();
			this.end_at_dest = false;
			if (this.following) {
				this.dest_position.x = this.following.current_position.x;
				this.dest_position.y = this.following.current_position.y;
			}
			else if (!behavior.follow && (behavior.x || behavior.y)) {
				this.end_at_dest = true;
				this.dest_position = {
					x: Math.round((winsize.width  - size.width)  * (behavior.x || 0) / 100),
					y: Math.round((winsize.height - size.height) * (behavior.y || 0) / 100)
				};
			}
			else {
				// reduce chance of going off-screen
				var movements = null;
				switch (behavior.movement) {
					case AllowedMoves.HorizontalOnly:
						movements = [Movements.Left, Movements.Right];
						break;

					case AllowedMoves.VerticalOnly:
						movements = [Movements.Up, Movements.Down];
						break;

					case AllowedMoves.HorizontalVertical:
						movements = [Movements.Left, Movements.Right,
						             Movements.Up, Movements.Down];
						break;

					case AllowedMoves.DiagonalOnly:
						movements = [Movements.UpLeft, Movements.UpRight,
						             Movements.DownLeft, Movements.DownRight];
						break;

					case AllowedMoves.DiagonalHorizontal:
						movements = [Movements.Left, Movements.Right,
						             Movements.UpLeft, Movements.UpRight,
						             Movements.DownLeft, Movements.DownRight];
						break;

					case AllowedMoves.DiagonalVertical:
						movements = [Movements.Up, Movements.Down,
						             Movements.UpLeft, Movements.UpRight,
						             Movements.DownLeft, Movements.DownRight];
						break;

					case AllowedMoves.All:
						movements = [Movements.Left, Movements.Right,
						             Movements.Up, Movements.Down,
						             Movements.UpLeft, Movements.UpRight,
						             Movements.DownLeft, Movements.DownRight];
						break;
				}

				if (movements === null) {
					this.dest_position.x = Math.round(pos.x);
					this.dest_position.y = Math.round(pos.y);
				}
				else {
					var nearTop    = pos.y - size.height * 0.5 < 100;
					var nearBottom = pos.y + size.height * 0.5 + 100 > winsize.height;
					var nearLeft   = pos.x - size.width * 0.5 < 100;
					var nearRight  = pos.x + size.width * 0.5 + 100 > winsize.width;
					var reducedMovements = movements.slice();

					if (nearTop) {
						removeAll(reducedMovements, Movements.Up);
						removeAll(reducedMovements, Movements.UpLeft);
						removeAll(reducedMovements, Movements.UpRight);
					}
					
					if (nearBottom) {
						removeAll(reducedMovements, Movements.Down);
						removeAll(reducedMovements, Movements.DownLeft);
						removeAll(reducedMovements, Movements.DownRight);
					}
					
					if (nearLeft) {
						removeAll(reducedMovements, Movements.Left);
						removeAll(reducedMovements, Movements.UpLeft);
						removeAll(reducedMovements, Movements.DownLeft);
					}
					
					if (nearRight) {
						removeAll(reducedMovements, Movements.Right);
						removeAll(reducedMovements, Movements.UpRight);
						removeAll(reducedMovements, Movements.DownRight);
					}

					// speed is in pixels/100ms, duration is in sec
					var dist = behavior.speed * duration * 100 * globalSpeed;

					var a;
					switch (randomSelect(reducedMovements.length === 0 ? movements : reducedMovements)) {
						case Movements.Up:
							this.dest_position = {
								x: pos.x,
								y: pos.y - dist
							};
							break;
						case Movements.Down:
							this.dest_position = {
								x: pos.x,
								y: pos.y + dist
							};
							break;
						case Movements.Left:
							this.dest_position = {
								x: pos.x - dist,
								y: pos.y
							};
							break;
						case Movements.Right:
							this.dest_position = {
								x: pos.x + dist,
								y: pos.y
							};
							break;
						case Movements.UpLeft:
							a = Math.sqrt(dist*dist*0.5);
							this.dest_position = {
								x: pos.x - a,
								y: pos.y - a
							};
							break;
						case Movements.UpRight:
							a = Math.sqrt(dist*dist*0.5);
							this.dest_position = {
								x: pos.x + a,
								y: pos.y - a
							};
							break;
						case Movements.DownLeft:
							a = Math.sqrt(dist*dist*0.5);
							this.dest_position = {
								x: pos.x - a,
								y: pos.y + a
							};
							break;
						case Movements.DownRight:
							a = Math.sqrt(dist*dist*0.5);
							this.dest_position = {
								x: pos.x + a,
								y: pos.y + a
							};
							break;
					}

					if (moveIntoScreen) {
						this.dest_position = clipToScreen(extend(this.dest_position, size));
						this.end_at_dest   = true;
					}
					else {
						// clipToScreen already rounds
						this.dest_position.x = Math.round(this.dest_position.x);
						this.dest_position.y = Math.round(this.dest_position.y);
					}
				}
			}

			// this changes the image to the new behavior:
			this.setFacingRight(
				pos.x !== this.dest_position.x ?
				pos.x <= this.dest_position.x :
				this.facing_right);

			// this initializes the new images position:
			// (alternatively maybe this.update(...) could be called?)
			this.setPosition(this.current_position);

			var overlay = getOverlay();
			this.repeating = [];
			for (var i = 0, n = behavior.effects.length; i < n; ++ i) {
				var effect = behavior.effects[i];
				var inst = new EffectInstance(this, this.start_time, effect);
				overlay.appendChild(inst.img);
				inst.updatePosition(this.start_time, 0);
				neweffects.push(inst);

				if (effect.delay) {
					this.repeating.push({
						effect: effect,
						at: this.start_time + effect.delay * 1000
					});
				}
			}
			this.effects = neweffects;
/*
			var msg;
			if (this.following) {
				msg = "following "+behavior.follow;
			}
			else {
				if (this.dest_position.x !== pos.x || this.dest_position.y !== pos.y) {
					msg = "move from "+pos.x+" x "+pos.y+" to "+
						Math.round(this.dest_position.x)+" x "+
						Math.round(this.dest_position.y);
				}
				else {
					msg = "no movement";
				}
				
				if (behavior.follow) {
					msg += " (wanted to follow "+behavior.follow+")";
				}
			}
			console.log(this.pony.name+" does "+behavior.name+": "+msg+" in "+duration+
				" seconds");
*/
		},
		teleport: function () {
			var winsize = windowSize();
			var size = this.size();
			this.setTopLeftPosition({
				x: Math.random() * (winsize.width  - size.width),
				y: Math.random() * (winsize.height - size.height)
			});
		},
		speakRandom: function (start_time, speak_probability) {
			if (Math.random() >= speak_probability) return;
			var filtered = [];
			var current_group = this.current_behavior.group;
			for (var i = 0, n = this.pony.random_speeches.length; i < n; ++ i) {
				var speech = this.pony.random_speeches[i];
				if (speech.group === 0 || speech.group === current_group) {
					filtered.push(speech);
				}
			}
			if (filtered.length > 0) {
				this.speak(start_time, randomSelect(filtered));
			}
		},
		randomBehavior: function (forceMovement) {
			var behaviors;
			var current_group = this.current_behavior ? this.current_behavior.group : 0;
			
			if (this === dragged && this.canDrag()) {
				behaviors = this.pony.dragged_behaviors;
			}
			else if (this.mouseover && this.canMouseOver()) {
				behaviors = this.pony.mouseover_behaviors;
			}
			else {
				behaviors = this.pony.random_behaviors;
			}

			var sumprob = 0;
			var filtered = [];
			for (var i = 0, n = behaviors.length; i < n; ++ i) {
				var behavior = behaviors[i];
				// don't filter looping behaviors because getNearestInstance filteres
				// looping instances and then it just degrades to a standard behavior
				if (forceMovement && !behavior.isMoving()) continue;
				if (current_group !== 0 && behavior.group !== 0 && behavior.group !== current_group) continue;
				sumprob += behavior.probability;
				filtered.push(behavior);
			}
			var dice = Math.random() * sumprob;
			var diceiter = 0;
			for (var i = 0, n = filtered.length; i < n; ++ i) {
				var behavior = filtered[i];
				diceiter += behavior.probability;
				if (dice <= diceiter) {
					return behavior;
				}
			}
			return forceMovement ? this.randomBehavior(false) : null;
		},
		loops: function (instance) {
			while (instance) {
				if (this === instance) return true;
				instance = instance.following;
			}
			return false;
		}
	});

	var EffectInstance = function EffectInstance (pony, start_time, effect) {
		this.pony       = pony;
		this.start_time = start_time;
		var duration = effect.duration * 1000;
		// XXX: Gecko gif animations speed is buggy!
		if (Gecko) duration *= 0.6;
		duration = Math.max(duration - fadeDuration, fadeDuration);
		this.end_time = start_time + duration;
		this.effect   = effect;
		
		var imgurl;
		if (pony.facing_right) {
			imgurl = this.effect.rightimage;
			this.current_size   = this.effect.rightsize;
			this.current_center = this.effect.rightcenter_point;
		}
		else {
			imgurl = this.effect.leftimage;
			this.current_size   = this.effect.leftsize;
			this.current_center = this.effect.leftcenter_point;
		}
		this.current_position = {x: 0, y: 0};
		this.zIndex = BaseZIndex;

		this.current_imgurl = null;
		this.img = this.createImage(imgurl);

		var locs = ['rightloc','rightcenter','leftloc','leftcenter'];
		for (var i = 0, n = locs.length; i < n; ++ i) {
			var name = locs[i];
			var loc = effect[name];

			if (loc === Locations.Any) {
				loc = randomSelect([
					Locations.Top, Locations.Bottom, Locations.Left, Locations.Right,
					Locations.BottomRight, Locations.BottomLeft, Locations.TopRight, Locations.TopLeft,
					Locations.Center
				]);
			}
			else if (loc === Locations.AnyNotCenter) {
				loc = randomSelect([
					Locations.Top, Locations.Bottom, Locations.Left, Locations.Right,
					Locations.BottomRight, Locations.BottomLeft, Locations.TopRight, Locations.TopLeft
				]);
			}

			this[name] = loc;
		}
	};

	EffectInstance.prototype = extend(new Instance(), {
		createImage: function (src) {
			var img = tag(Gecko || Opera ? 'img' : 'iframe', {
				src: src,
				draggable: 'false',
				style: {
					position:        "fixed",
					overflow:        "hidden",
					userSelect:      "none",
					pointerEvents:   "none",
					borderStyle:     "none",
					margin:          "0",
						padding:         "0",
					backgroundColor: "transparent",
					width:           this.current_size.width+"px",
					height:          this.current_size.height+"px",
					zIndex:          String(BaseZIndex)
				}});
			if (IE) {
				img.setAttribute("scrolling",   "no");
				img.setAttribute("frameborder",  "0");
				img.setAttribute("marginheight", "0");
				img.setAttribute("marginwidth",  "0");
			}
			return img;
		},
		name: function () {
			return this.effect.name;
		},
		clear: function () {
			if (this.img.parentNode) {
				this.img.parentNode.removeChild(this.img);
			}
		},
		updatePosition: function (currentTime, passedTime) {
			var loc, center;
			if (this.pony.facing_right) {
				loc = this.rightloc;
				center = this.rightcenter;
			}
			else {
				loc = this.leftloc;
				center = this.leftcenter;
			}

			var size = this.size();
			var pos;

			switch (center) {
				case Locations.Top:
					pos = {x: -size.width * 0.5, y: 0};
					break;
				case Locations.Bottom:
					pos = {x: -size.width * 0.5, y: -size.height};
					break;
				case Locations.Left:
					pos = {x: 0, y: -size.height * 0.5};
					break;
				case Locations.Right:
					pos = {x: -size.width, y: -size.height * 0.5};
					break;
				case Locations.BottomRight:
					pos = {x: -size.width, y: -size.height};
					break;
				case Locations.BottomLeft:
					pos = {x: 0, y: -size.height};
					break;
				case Locations.TopRight:
					pos = {x: -size.width, y: 0};
					break;
				case Locations.TopLeft:
					pos = {x: 0, y: 0};
					break;
				case Locations.Center:
					pos = {x: -size.width * 0.5, y: -size.height * 0.5};
					break;
			}
			
			var ponyRect = this.pony.topLeftRect();
			switch (loc) {
				case Locations.Top:
					pos.x += ponyRect.x + ponyRect.width * 0.5;
					pos.y += ponyRect.y;
					break;
				case Locations.Bottom:
					pos.x += ponyRect.x + ponyRect.width * 0.5;
					pos.y += ponyRect.y + ponyRect.height;
					break;
				case Locations.Left:
					pos.x += ponyRect.x;
					pos.y += ponyRect.y + ponyRect.height * 0.5;
					break;
				case Locations.Right:
					pos.x += ponyRect.x + ponyRect.width;
					pos.y += ponyRect.y + ponyRect.height * 0.5;
					break;
				case Locations.BottomRight:
					pos.x += ponyRect.x + ponyRect.width;
					pos.y += ponyRect.y + ponyRect.height;
					break;
				case Locations.BottomLeft:
					pos.x += ponyRect.x;
					pos.y += ponyRect.y + ponyRect.height;
					break;
				case Locations.TopRight:
					pos.x += ponyRect.x + ponyRect.width;
					pos.y += ponyRect.y;
					break;
				case Locations.TopLeft:
					pos.x += ponyRect.x;
					pos.y += ponyRect.y;
					break;
				case Locations.Center:
					pos.x += ponyRect.x + ponyRect.width  * 0.5;
					pos.y += ponyRect.y + ponyRect.height * 0.5;
					break;
			}

			this.setTopLeftPosition(pos);
		},
		/*
		setImage: function (url) {
			if (this.current_imgurl !== url) {
				this.img.src = dataUrl('text/html',
					'<html><head><title>'+Math.random()+
					'</title><style text="text/css">html,body{margin:0;padding:0;background:transparent;}</style><body></body><img src="'+
					escapeXml(URL.abs(url))+'"/></html>');
				this.img.style.width  = this.current_size.width+"px";
				this.img.style.height = this.current_size.height+"px";
				this.current_imgurl = url;
			}
		},
		*/
		setImage: Gecko ?
		function (url) {
			if (this.current_imgurl !== url) {
				// gif animation bug workaround
				var img = this.createImage(url);
				img.style.left   = this.img.style.left;
				img.style.top    = this.img.style.top;
				img.style.zIndex = this.img.style.zIndex;
				this.current_imgurl = url;
				this.img.parentNode.replaceChild(img, this.img);
				this.img = img;
			}
		} :
		function (url) {
			if (this.current_imgurl !== url) {
				this.img.src = this.current_imgurl = url;
				this.img.style.width  = this.current_size.width+"px";
				this.img.style.height = this.current_size.height+"px";
			}
		},
		update: function (currentTime, passedTime, winsize) {
			if (this.effect.follow) {
				this.updatePosition(currentTime, passedTime);
				
				var imgurl;
				if (this.pony.facing_right) {
					imgurl = this.effect.rightimage;
					this.current_size   = this.effect.rightsize;
					this.current_center = this.effect.rightcenter_point;
				}
				else {
					imgurl = this.effect.leftimage;
					this.current_size   = this.effect.leftsize;
					this.current_center = this.effect.leftcenter_point;
				}
				this.setImage(imgurl);
			}
			return !this.effect.duration || currentTime < this.end_time;
		}
	});

	var lastTime = Date.now();
	var tick = function () {
		if (timer === null) return;
		var currentTime = Date.now();
		var timeSpan = currentTime - lastTime;
		var winsize = windowSize();
		
		for (var i = 0, n = instances.length; i < n; ++ i) {
			instances[i].update(currentTime, timeSpan, winsize);
		}

		// check if something needs to be removed:
		for (var i = 0; i < removing.length;) {
			var what = removing[i];
			if (what.at + fadeDuration <= currentTime) {
				if (what.element.parentNode) {
					what.element.parentNode.removeChild(what.element);
				}
				removing.splice(i, 1);
			}
			else {
				if (what.at <= currentTime) {
					setOpacity(what.element, 1 - (currentTime - what.at) / fadeDuration);
				}
				++ i;
			}
		}

		if (showFps) {
			if (!fpsDisplay) {
				var overlay = getOverlay();
				fpsDisplay = tag('div', {style: {
					fontSize:  '18px',
					position: 'fixed',
					bottom:       '0',
					left:         '0',
					zIndex: String(BaseZIndex + 9001)
				}});
				overlay.appendChild(fpsDisplay);
			}

			fpsDisplay.innerHTML = Math.round(1000 / timeSpan) + ' fps';
		}

		timer = setTimeout(tick, Math.max(interval - (currentTime - Date.now()), 0));

		lastTime = currentTime;
	};

	var fadeDuration = 500;
	var preloadAll = false;
	var showLoadProgress = true;
	var audioEnabled = false;
	var showFps = false;
	var globalBaseUrl = URL.abs('');
	var globalSpeed = 3; // why is it too slow otherwise?
	var speakProbability = 0.1;
	var dontSpeak = false;
	var interval = 40;
	var interactionInterval = 500;
	var ponies = {};
	var instances = [];
	var removing = [];
	var overlay = null;
	var timer = null;
	var mousePosition = null;
	var dragged = null;
	var fpsDisplay = null;
	var volume = 1.0;

	var getOverlay = function () {
		if (!overlay) {
			overlay = tag('div', {id: 'browser-ponies'});
		}
		if (!overlay.parentNode) {
			document.body.appendChild(overlay);
		}
		return overlay;
	};

	observe(document, 'touchstart', function (event) {
		mousePosition = null;
	});
	observe(document, 'mousemove', function (event) {
		if (!mousePosition) {
			mousePosition = {
				x: event.clientX,
				y: event.clientY
			};
		}
		if (dragged) {
			dragged.moveBy({
				x: event.clientX - mousePosition.x,
				y: event.clientY - mousePosition.y
			});
			extend(dragged.dest_position, dragged.current_position);
			event.preventDefault();
		}
		mousePosition.x = event.clientX;
		mousePosition.y = event.clientY;
	});
	
	observe(document, 'mouseup', function () {
		if (dragged) {
			var inst = dragged;
			dragged = null;
			if (timer !== null) {
				inst.nextBehavior();
			}
		}
	});
	
	return {
		convertPony: function (ini, baseurl) {
			var rows = PonyINI.parse(ini);
			var pony = {
				baseurl:    baseurl || "",
				behaviorgroups: {},
				behaviors:  [],
				speeches:   [],
				categories: []
			};
			var behaviors_by_name = {};
			var effects = [];

			for (var i = 0, n = rows.length; i < n; ++ i) {
				var row = rows[i];
				var type = row[0].toLowerCase();

				switch (type) {
					case "name":
						pony.name = row[1];
						break;
					
					case "behaviorgroup":
						var group = parseInt(row[1],10);
						if (isNaN(group)) {
							console.warn(baseurl+': illegal behavior group id: ',row[1]);
						}
						else {
							pony.behaviorgroups[group] = row[2];
						}
						break;

					case "behavior":
						var behavior = {
							name: row[1],
							probability: Number(row[2]),
							maxduration: Number(row[3]),
							minduration: Number(row[4]),
							speed:       Number(row[5]),
							rightimage:  encodeURIComponent(row[6]),
							leftimage:   encodeURIComponent(row[7]),
							movement:    row[8],
							effects:     [],
							auto_select_images:    true,
							dont_repeat_animation: false // XXX: cannot be supported by JavaScript
						};
						if (row.length > 9) {
							if (row[9]) behavior.linked = row[9];
							var speakstart = (row[10] || '').trim();
							if (speakstart) behavior.speakstart = speakstart;
							var speakend = (row[11] || '').trim();
							if (speakend)   behavior.speakend   = speakend;
							behavior.skip = parseBoolean(row[12]);
							behavior.x    = Number(row[13]);
							behavior.y    = Number(row[14]);
							if (row[15]) behavior.follow = row[15];

							if (row.length > 16) {
								behavior.auto_select_images = parseBoolean(row[16]);
								if (row[17]) behavior.stopped = row[17];
								if (row[18]) behavior.moving  = row[18];

								if (row.length > 19) {
									behavior.rightcenter = parsePoint(row[19]);
									behavior.leftcenter  = parsePoint(row[20]);

									if (row.length > 21) {
										behavior.dont_repeat_animation = parseBoolean(row[21]);
										if (behavior.dont_repeat_animation) {
											console.warn(baseurl+': behavior '+behavior.name+
												' sets dont_repeat_animation to true, which is not supported by Browser Ponies due to limitations in browsers. '+
												'Please use a GIF that does not loop instead.');
										}
										if (row[22]) {
											behavior.group = parseInt(row[22],10);
											if (isNaN(behavior.group)) {
												delete behavior.group;
												console.warn(baseurl+': behavior '+behavior.name+
													' references illegal behavior group id: ',row[22]);
											}
										}
									}
								}
							}
						}
						pony.behaviors.push(behavior);
						behaviors_by_name[behavior.name.toLowerCase()] = behavior;
						break;
						
					case "effect":
						var effect = {
							name:        row[1],
							behavior:    row[2],
							rightimage:  encodeURIComponent(row[3]),
							leftimage:   encodeURIComponent(row[4]),
							duration:    Number(row[5]),
							delay:       Number(row[6]),
							rightloc:    row[7].trim(),
							rightcenter: row[8].trim(),
							leftloc:     row[9].trim(),
							leftcenter:  row[10].trim(),
							follow:      parseBoolean(row[11]),
							dont_repeat_animation: row[12] ? parseBoolean(row[12]) : false // XXX: cannot be supported by JavaScript
						};
						if (effect.dont_repeat_animation) {
							console.warn(baseurl+': effect '+effect.name+
								' sets dont_repeat_animation to true, which is not supported by Browser Ponies due to limitations in browsers. '+
								'Please use a GIF that does not loop instead.');
						}
						effects.push(effect);
						break;
						
					case "speak":
						var speak;
						if (row.length === 2) {
							speak = {
								text: row[1]
							};
						}
						else {
							speak = {
								name: row[1],
								text: row[2].trim()
							};
							if (row[4]) speak.skip  = parseBoolean(row[4]);
							if (row[5]) speak.group = parseInt(row[5],10);
							var files = row[3];
							if (files) {
								if (!Array.isArray(files)) files = [files];
								if (files.length > 0) {
									speak.files = {};
									for (var j = 0; j < files.length; ++ j) {
										var file = files[j];
										var ext = /(?:\.([^\.]*))?$/.exec(file)[1];
										var filetype;
										if (ext) {
											ext = ext.toLowerCase();
											filetype = AudioMimeTypes[ext] || 'audio/x-'+ext;
										}
										else {
											filetype = 'audio/x-unknown';
										}
										if (filetype in speak.files) {
											console.warn(baseurl+': file type '+filetype+
												' of speak line '+speak.name+
												' is not unique.');
										}
										speak.files[filetype] = encodeURIComponent(file);
									}
								}
							}
							if ('group' in speak && isNaN(speak.group)) {
								delete speak.group;
								console.warn(baseurl+': speak line '+speak.name+
									' references illegal behavior group id: ',row[5]);
							}
						}
						pony.speeches.push(speak);
						break;

					case "categories":
						pony.categories = pony.categories.concat(row.slice(1));
						break;

					default:
						console.warn(baseurl+": Unknown pony setting:",row);
				}
			}
			
			if (!('name' in pony)) {
				throw new Error('Pony with following base URL has no name: '+pony.baseurl);
			}

			for (var i = 0, n = effects.length; i < n; ++ i) {
				var effect = effects[i];
				var behavior = effect.behavior.toLowerCase();
				if (!has(behaviors_by_name,behavior)) {
					console.warn(baseurl+": Effect "+effect.name+" of pony "+pony.name+
						" references non-existing behavior "+effect.behavior);
				}
				else {
					behaviors_by_name[behavior].effects.push(effect);
					delete effect.behavior;
				}
			}

			for (var name in behaviors_by_name) {
				var behavior = behaviors_by_name[name];
				if (behavior.effects.length === 0) {
					delete behavior.effects;
				}
			}

			var has_behaviorgroups = false;
			for (var behaviorgroup in pony.behaviorgroups) {
				has_behaviorgroups = true;
				break;
			}

			if (!has_behaviorgroups) {
				delete pony.behaviorgroups;
			}

			return pony;
		},
		convertInteractions: function (ini) {
			var rows = PonyINI.parse(ini);
			var interactions = [];

			for (var i = 0, n = rows.length; i < n; ++ i) {
				var row = rows[i];
				var activate = "one";
				if (row.length > 4) {
					activate = row[5].trim().toLowerCase();
					if (activate === "true" || activate === "all") {
						activate = "all";
					}
					else if (activate == "random" || activate === "any") {
						activate = "any";
					}
					else if (activate === "false" || activate === "one") {
						activate = "one";
					}
					else {
						throw new Error("illegal target activation value: "+row[5]);
					}
				}

				var proximity = row[3].trim().toLowerCase();
				if (proximity !== "default") proximity = Number(proximity);
				interactions.push({
					name:        row[0],
					pony:        row[1],
					probability: Number(row[2]),
					proximity:   proximity,
					targets:     row[4],
					activate:    activate,
					behaviors:   row[6],
					delay:       row.length > 7 ? Number(row[7].trim()) : 0
				});
			}

			return interactions;
		},
		addInteractions: function (interactions) {
			if (typeof(interactions) === "string") {
				interactions = this.convertInteractions(interactions);
			}
			for (var i = 0, n = interactions.length; i < n; ++ i) {
				this.addInteraction(interactions[i]);
			}
		},
		addInteraction: function (interaction) {
			var lowername = interaction.pony.toLowerCase();
			if (!has(ponies,lowername)) {
				console.error("No such pony:",interaction.pony);
				return false;
			}
			return ponies[lowername].addInteraction(interaction);
		},
		addPonies: function (ponies) {
			for (var i = 0, n = ponies.length; i < n; ++ i) {
				this.addPony(ponies[i]);
			}
		},
		addPony: function (pony) {
			if (pony.ini) {
				pony = this.convertPony(pony.ini, pony.baseurl);
			}
			if (pony.behaviors.length === 0) {
				console.error("Pony "+pony.name+" has no behaviors.");
				return false;
			}
			var lowername = pony.name.toLowerCase();
			if (has(ponies,lowername)) {
				console.error("Pony "+pony.name+" already exists.");
				return false;
			}
			ponies[lowername] = new Pony(pony);
			return true;
		},
		removePonies: function (ponies) {
			for (var i = 0, n = ponies.length; i < n; ++ i) {
				this.removePony(ponies[i]);
			}
		},
		removePony: function (name) {
			var lowername = name.toLowerCase();
			if (has(ponies,lowername)) {
				ponies[lowername].unspawnAll();
				delete ponies[lowername];
			}
		},
		spawnRandom: function (count) {
			if (count === undefined) count = 1;
			else count = parseInt(count);

			if (isNaN(count)) {
				console.error("unexpected NaN value");
				return [];
			}

			var spawned = [];
			while (count > 0) {
				var mininstcount = Infinity;

				for (var name in ponies) {
					var instcount = ponies[name].instances.length;
					if (instcount < mininstcount) {
						mininstcount = instcount;
					}
				}

				if (mininstcount === Infinity) {
					console.error("can't spawn random ponies because there are no ponies loaded");
					break;
				}

				var names = [];
				for (var name in ponies) {
					if (ponies[name].instances.length === mininstcount) {
						names.push(name);
					}
				}

				var name = randomSelect(names);

				if (this.spawn(name)) {
					spawned.push(name);
				}
				-- count;
			}
			return spawned;
		},
		spawn: function (name, count) {
			var lowername = name.toLowerCase();
			if (!has(ponies,lowername)) {
				console.error("No such pony:",name);
				return false;
			}
			var pony = ponies[lowername];
			if (count === undefined) {
				count = 1;
			}
			else {
				count = parseInt(count);
				if (isNaN(count)) {
					console.error("unexpected NaN value");
					return false;
				}
			}

			if (count > 0 && timer !== null) {
				pony.preload();
			}
			var n = count;
			while (n > 0) {
				var inst = new PonyInstance(pony);
				pony.instances.push(inst);
				if (timer !== null) {
					onload(function () {
						if (this.pony.instances.indexOf(this) === -1) return;
						instances.push(this);
						this.img.style.visibility = 'hidden';
						getOverlay().appendChild(this.img);
						this.teleport();
						this.nextBehavior();
						// fix position because size was initially 0x0
						this.clipToScreen();
						this.img.style.visibility = '';
					}.bind(inst));
				}
				else {
					instances.push(inst);
				}
				-- n;
			}
			return true;
		},
		unspawn: function (name, count) {
			var lowername = name.toLowerCase();
			if (!has(ponies,lowername)) {
				console.error("No such pony:",name);
				return false;
			}
			var pony = ponies[lowername];
			if (count === undefined) {
				count = pony.instances.length;
			}
			else {
				count = parseInt(count);
				if (isNaN(count)) {
					console.error("unexpected NaN value");
					return false;
				}
			}
			if (count >= pony.instances.length) {
				pony.unspawnAll();
			}
			else {
				while (count > 0) {
					pony.instances[pony.instances.length - 1].unspawn();
					-- count;
				}
			}
			return true;
		},
		unspawnAll: function () {
			for (var name in ponies) {
				ponies[name].unspawnAll();
			}
		},
		clear: function () {
			this.unspawnAll();
			ponies = {};
		},
		preloadAll: function () {
			for (var name in ponies) {
				ponies[name].preload();
			}
		},
		preloadSpawned: function () {
			for (var name in ponies) {
				var pony = ponies[name];
				if (pony.instances.length > 0) {
					pony.preload();
				}
			}
		},
		start: function () {
			if (preloadAll) {
				this.preloadAll();
			}
			else {
				this.preloadSpawned();
			}
			onload(function () {
				var overlay = getOverlay();
				overlay.innerHTML = '';
				for (var i = 0, n = instances.length; i < n; ++ i) {
					var inst = instances[i];
					inst.clear();
					inst.img.style.visibility = 'hidden';
					overlay.appendChild(inst.img);
					inst.teleport();
					inst.nextBehavior();
					// fix position because size was initially 0x0
					inst.clipToScreen();
					inst.img.style.visibility = '';
				}
				if (timer === null) {
					lastTime = Date.now();
					timer = setTimeout(tick, 0);
				}
			});
		},
		timer: function () {
			return timer;
		},
		stop: function () {
			if (overlay) {
				overlay.parentNode.removeChild(overlay);
				overlay.innerHTML = '';
				overlay = null;
			}
			fpsDisplay = null;
			if (timer !== null) {
				clearTimeout(timer);
				timer = null;
			}
		},
		pause: function () {
			if (timer !== null) {
				clearTimeout(timer);
				timer = null;
			}
		},
		resume: function () {
			if (preloadAll) {
				this.preloadAll();
			}
			else {
				this.preloadSpawned();
			}
			onload(function () {
				if (timer === null) {
					lastTime = Date.now();
					timer = setTimeout(tick, 0);
				}
			});
		},
		setInterval: function (ms) {
			ms = parseInt(ms);
			if (isNaN(ms)) {
				console.error("unexpected NaN value for interval");
			}
			else if (interval !== ms) {
				interval = ms;
			}
		},
		getInterval: function () {
			return interval;
		},
		setFps: function (fps) {
			this.setInterval(1000 / Number(fps));
		},
		getFps: function () {
			return 1000 / interval;
		},
		setInteractionInterval: function (ms) {
			ms = Number(ms);
			if (isNaN(ms)) {
				console.error("unexpected NaN value for interaction interval");
			}
			else {
				interactionInterval = ms;
			}
		},
		getInteractionInterval: function () {
			return interactionInterval;
		},
		setSpeakProbability: function (probability) {
			probability = Number(probability);
			if (isNaN(probability)) {
				console.error("unexpected NaN value for speak probability");
			}
			else {
				speakProbability = probability;
			}
		},
		getSpeakProbability: function () {
			return speakProbability;
		},
		setDontSpeak: function (value) {
			dontSpeak = !!value;
		},
		isDontSpeak: function () {
			return dontSpeak;
		},
		setVolume: function (value) {
			value = Number(value);
			if (isNaN(value)) {
				console.error("unexpected NaN value for volume");
			}
			else if (value < 0 || value > 1) {
				console.error("volume out of range", value);
			}
			else {
				volume = value;
			}

		},
		getVolume: function () {
			return volume;
		},
		setBaseUrl: function (url) {
			globalBaseUrl = URL.fix(url);
		},
		getBaseUrl: function () {
			return globalBaseUrl;
		},
		setSpeed: function (speed) {
			globalSpeed = Number(speed);
		},
		getSpeed: function () {
			return globalSpeed;
		},
		setAudioEnabled: function (enabled) {
			if (typeof(enabled) === "string") {
				try {
					enabled = parseBoolean(enabled);
				}
				catch (e) {
					console.error("illegal value for audio enabled",enabled,e);
					return;
				}
			}
			else {
				enabled = !!enabled;
			}
			if (audioEnabled !== enabled && enabled) {
				audioEnabled = enabled;
				if (preloadAll) {
					this.preloadAll();
				}
				else {
					this.preloadSpawned();
				}
			}
			else {
				audioEnabled = enabled;
			}
		},
		isAudioEnabled: function () {
			return audioEnabled;
		},
		setShowFps: function (value) {
			if (typeof(value) === "string") {
				try {
					showFps = parseBoolean(value);
				}
				catch (e) {
					console.error("illegal value for show fps",value,e);
					return;
				}
			}
			else {
				showFps = !!value;
			}
			if (!showFps && fpsDisplay) {
				if (fpsDisplay.parentNode) {
					fpsDisplay.parentNode.removeChild(fpsDisplay);
				}
				fpsDisplay = null;
			}
		},
		isShowFps: function () {
			return showFps;
		},
		setPreloadAll: function (all) {
			if (typeof(all) === "string") {
				try {
					preloadAll = parseBoolean(all);
				}
				catch (e) {
					console.error("illegal value for preload all",all,e);
					return;
				}
			}
			else {
				preloadAll = !!all;
			}
		},
		isPreloadAll: function () {
			return preloadAll;
		},
		setShowLoadProgress: function (show) {
			if (typeof(show) === "string") {
				try {
					showLoadProgress = parseBoolean(show);
				}
				catch (e) {
					console.error(e);
					return;
				}
			}
			else {
				showLoadProgress = !!show;
			}
		},
		isShowLoadProgress: function () {
			return showLoadProgress;
		},
		getFadeDuration: function () {
			return fadeDuration;
		},
		setFadeDuration: function (ms) {
			fadeDuration = Number(ms);
		},
		running: function () {
			return timer !== null;
		},
		ponies: function () {
			return ponies;
		},
		loadConfig: function (config) {
			if ('baseurl' in config) {
				this.setBaseUrl(config.baseurl);
			}
			if ('speed' in config) {
				this.setSpeed(config.speed);
			}
			if ('speakProbability' in config) {
				this.setSpeakProbability(config.speakProbability);
			}
			if ('dontSpeak' in config) {
				this.setDontSpeak(config.dontSpeak);
			}
			if ('volume' in config) {
				this.setVolume(config.volume);
			}
			if ('interval' in config) {
				this.setInterval(config.interval);
			}
			if ('fps' in config) {
				this.setFps(config.fps);
			}
			if ('interactionInterval' in config) {
				this.setInteractionInterval(config.interactionInterval);
			}
			if ('audioEnabled' in config) {
				this.setAudioEnabled(config.audioEnabled);
			}
			if ('showFps' in config) {
				this.setShowFps(config.showFps);
			}
			if ('preloadAll' in config) {
				this.setPreloadAll(config.preloadAll);
			}
			if ('showLoadProgress' in config) {
				this.setShowLoadProgress(config.showLoadProgress);
			}
			if ('fadeDuration' in config) {
				this.setFadeDuration(config.fadeDuration);
			}
			if (config.ponies) {
				this.addPonies(config.ponies);
			}
			if (config.interactions) {
				this.addInteractions(config.interactions);
			}
			if (config.spawn) {
				for (var name in config.spawn) {
					this.spawn(name, config.spawn[name]);
				}
			}
			if ('spawnRandom' in config) {
				this.spawnRandom(config.spawnRandom);
			}
			if (config.onload) {
				if (Array.isArray(config.onload)) {
					for (var i = 0, n = config.onload.length; i < n; ++ i) {
						onload(config.onload[i]);
					}
				}
				else {
					onload(config.onload);
				}
			}
			if (config.autostart && timer === null) {
				this.start();
			}
		},
		// currently excluding ponies and interactions
		dumpConfig: function () {
			var config = {};
			config.baseurl = this.getBaseUrl();
			config.speed = this.getSpeed();
			config.speakProbability = this.getSpeakProbability();
			config.dontSpeak = this.isDontSpeak();
			config.volume = this.getVolume();
			config.interval = this.getInterval();
			config.fps = this.getFps();
			config.interactionInterval = this.getInteractionInterval();
			config.audioEnabled = this.isAudioEnabled();
			config.showFps = this.isShowFps();
			config.preloadAll = this.isPreloadAll();
			config.showLoadProgress = this.isShowLoadProgress();
			config.fadeDuration = this.getFadeDuration();
			// TODO: optionally dump ponies and interactions
			config.spawn = {};
			for (var name in ponies) {
				var pony = ponies[name];
				if (pony.instances.length > 0) {
					config.spawn[pony.name] = pony.instances.length;
				}
			}

			return config;
		},

		togglePoniesToBackground: function () {
			if (typeof(toggleBrowserPoniesToBackground) === "undefined") {
				alert("This website does not support bringing Browser Ponies to the background.");
			}
			else {
				try {
					toggleBrowserPoniesToBackground();
				}
				catch (e) {
					alert("Error toggling Browser Ponies to the background:\n\n"+e.name+': '+e.message);
				}
			}
		},

		// expose a few utils:
		Util: {
			setOpacity:    setOpacity,
			extend:        extend,
			tag:           extend(tag,{add:add}),
			has:           has,
			format:        format,
			partial:       partial,
			observe:       observe,
			stopObserving: stopObserving,
			IE:            IE,
			Opera:         Opera,
			Gecko:         Gecko,
			HasAudio:      HasAudio,
			BaseZIndex:    BaseZIndex,
			onload:        onload,
			onprogress:    onprogress,
			$:             $,
			randomSelect:  randomSelect,
			dataUrl:       dataUrl,
			escapeXml:     escapeXml,
			Base64:        Base64,
			PonyINI:       PonyINI,
			getOverlay:    getOverlay,
			parseBoolean:  parseBoolean,
			parsePoint:    parsePoint,
			URL:           URL
		}
	};
})();

if (typeof(BrowserPoniesConfig) !== "undefined") {
	BrowserPonies.loadConfig(BrowserPoniesConfig);
	if (BrowserPoniesConfig.oninit) {
		(function () {
			if (Array.isArray(BrowserPoniesConfig.oninit)) {
				for (var i = 0, n = BrowserPoniesConfig.oninit.length; i < n; ++ i) {
					BrowserPoniesConfig.oninit[i]();
				}
			}
			else {
				BrowserPoniesConfig.oninit();
			}
		})();
	}
}

}
