UNIFIED CheckPlay For Challenge Group Administration NG (beta)

By Alesa Dam Last update May 6, 2012 — Installed 2,649 times.

There are 142 previous versions of this script.

the source is over 100KB, syntax highlighting in the browser is too slow

// ==UserScript==
// @name           UNIFIED ChekPlay For Challenge Group Administration
// @namespace      http://www.flickr.com/alesadam
// @description    UNIFIED ChekPlay For Challenge Group Administration
// @author	   Alesa Dam (http://flickr.com/alesadam/)
// @contributor    Martin Heimburger (http://flickr.com/vispillo/)
// @contributor	   Kris Vangeel (http://flickr.com/kvgl69/)
// @date           03/1/2010
// @version        1.8.0
// @modified       May. 06, 2012
//
// @include        http://www.flickr.com/groups/*/discuss*
// @match          http://www.flickr.com/groups/*/discuss*
// @include        http://www.flickr.com/groups/*/admin/pending*
// @match          http://www.flickr.com/groups/*/admin/pending*
// @include        http://userscripts.org/*
// @match          http://userscripts.org/*
// @exclude        http://www.flickr.com/groups/*/?search*
//
// @run-at         document-end
//
// @copyright	2009-2012 Alesa Dam
// @license	GPL v3+
// @licstart	The following is the entire license notice for this script.
/*
 * Copyright (C) 2009-2012 Alesa Dam
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details 
 * at <http://www.gnu.org/licenses/>.
 */
// @licend	End of license
//
// ==/UserScript==
//
// Changelog on http://www.flickr.com/groups/unified_checkplay/discuss/72157623838711730/
//
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// --------------------------------------------------------------------
// desired features:
// - report threads in state OPEN, VOTE, .. but where the thread itself is closed
// - check how long a challenge has been 'open' => early voting, drop excludes, ..
// - implement challenge and rule overrides
//      . early voting: UCPoverride:photos:n + UCPoverride:votes:m === done
//      . example vote
//      . skip reply
//      . mark error as an ok
//      . excludes
// - add check on tags
// - check on number of running challenges (OPEN + VOTE + FINISHED)
// - sort discussion page (new --action--)
// - info pane: similar info as TMOACG tool
// - http://www.flickr.com/groups/1221507@N24/discuss/72157625112641483/
//
// known issues:
// - an admin could Approve his/her own entries
//
//---------------------
// If you have any suggestions for improvement, encounter bugs, have patches available , or you play in a 
// challenge group that is not supported, please FlickrMail me, and I will take care of it.
// We try to keep this as UNIFIED as possible.
// Contact: 
// - Alesa Dam (http://flickr.com/alesadam/)

/*jslint browser: true, maxerr: 500, onevar: false, regexp: false, plusplus: false, bitwise: false, newcap: false */
/*global window, document, GM_setValue, GM_getValue, GM_listValues, GM_deleteValue, GM_log, XPathResult, ucpUniversalHash, ucpCheckPhotoApproval*/

(function () {
var CPtoolversion="V1.8.0";
var scriptNumber = 66993;
var debug = false;
function GM_debug(message) {
    if (debug) {
        GM_log("UCPA DEBUG - " + message);
    }
}

// Greased MooTools:
/*
---

script: Core.js

description: The core of MooTools, contains all the base functions and the Native and Hash implementations. Required by all the other scripts.

license: MIT-style license.

copyright: Copyright (c) 2006-2008 [Valerio Proietti](http://mad4milk.net/).

authors: The MooTools production team (http://mootools.net/developers/)

inspiration:
- Class implementation inspired by [Base.js](http://dean.edwards.name/weblog/2006/03/base/) Copyright (c) 2006 Dean Edwards, [GNU Lesser General Public License](http://opensource.org/licenses/lgpl-license.php)
- Some functionality inspired by [Prototype.js](http://prototypejs.org) Copyright (c) 2005-2007 Sam Stephenson, [MIT License](http://opensource.org/licenses/mit-license.php)

provides: [MooTools, Native, Hash.base, Array.each, $util]

...
*/

var MooTools = {
	'version': '1.2.5dev',
	'build': '168759f5904bfdaeafd6b1c0d1be16cd78b5d5c6'
};

var Native = function(options){
	options = options || {};
	var name = options.name;
	var legacy = options.legacy;
	var protect = options.protect;
	var methods = options.implement;
	var generics = options.generics;
	var initialize = options.initialize;
	var afterImplement = options.afterImplement || function(){};
	var object = initialize || legacy;
	generics = generics !== false;

	object.constructor = Native;
	object.$family = {name: 'native'};
	if (legacy && initialize) object.prototype = legacy.prototype;
	if (!object.prototype) object.prototype = {};
	object.prototype.constructor = object;

	if (name){
		var family = name.toLowerCase();
		object.prototype.$family = {name: family};
		Native.typize(object, family);
	}

	var add = function(obj, name, method, force){
		if (!protect || force || !obj.prototype[name]) obj.prototype[name] = method;
		if (generics) Native.genericize(obj, name, protect);
		afterImplement.call(obj, name, method);
		return obj;
	};

	object.alias = function(a1, a2, a3){
		if (typeof a1 == 'string'){
			var pa1 = this.prototype[a1];
			if ((a1 = pa1)) return add(this, a2, a1, a3);
		}
		for (var a in a1) this.alias(a, a1[a], a2);
		return this;
	};

	object.implement = function(a1, a2, a3){
		if (typeof a1 == 'string') return add(this, a1, a2, a3);
		for (var p in a1) add(this, p, a1[p], a2);
		return this;
	};

	if (methods) object.implement(methods);

	return object;
};

Native.genericize = function(object, property, check){
	if ((!check || !object[property]) && typeof object.prototype[property] == 'function') object[property] = function(){
		var args = Array.prototype.slice.call(arguments);
		return object.prototype[property].apply(args.shift(), args);
	};
};

Native.implement = function(objects, properties){
	for (var i = 0, l = objects.length; i < l; i++) objects[i].implement(properties);
};

Native.typize = function(object, family){
	if (!object.type) object.type = function(item){
		return ($type(item) === family);
	};
};

(function(){
	var natives = {'Array': Array, 'Date': Date, 'Function': Function, 'Number': Number, 'RegExp': RegExp, 'String': String};
	for (var n in natives) new Native({name: n, initialize: natives[n], protect: true});

	var types = {'boolean': Boolean, 'native': Native, 'object': Object};
	for (var t in types) Native.typize(types[t], t);

	var generics = {
		'Array': ["concat", "indexOf", "join", "lastIndexOf", "pop", "push", "reverse", "shift", "slice", "sort", "splice", "toString", "unshift", "valueOf"],
		'String': ["charAt", "charCodeAt", "concat", "indexOf", "lastIndexOf", "match", "replace", "search", "slice", "split", "substr", "substring", "toLowerCase", "toUpperCase", "valueOf"]
	};
	for (var g in generics){
		for (var i = generics[g].length; i--;) Native.genericize(natives[g], generics[g][i], true);
	}
})();

var Hash = new Native({

	name: 'Hash',

	initialize: function(object){
		if ($type(object) == 'hash') object = $unlink(object.getClean());
		for (var key in object) this[key] = object[key];
		return this;
	}

});

Hash.implement({

	forEach: function(fn, bind){
		for (var key in this){
			if (this.hasOwnProperty(key)) fn.call(bind, this[key], key, this);
		}
	},

	getClean: function(){
		var clean = {};
		for (var key in this){
			if (this.hasOwnProperty(key)) clean[key] = this[key];
		}
		return clean;
	},

	getLength: function(){
		var length = 0;
		for (var key in this){
			if (this.hasOwnProperty(key)) length++;
		}
		return length;
	}

});

Hash.alias('forEach', 'each');

Array.implement({

	forEach: function(fn, bind){
		for (var i = 0, l = this.length; i < l; i++) fn.call(bind, this[i], i, this);
	}

});

Array.alias('forEach', 'each');

function $A(iterable){
	if (iterable.item){
		var l = iterable.length, array = new Array(l);
		while (l--) array[l] = iterable[l];
		return array;
	}
	return Array.prototype.slice.call(iterable);
};

function $arguments(i){
	return function(){
		return arguments[i];
	};
};

function $chk(obj){
	return !!(obj || obj === 0);
};

function $clear(timer){
	clearTimeout(timer);
	clearInterval(timer);
	return null;
};

function $defined(obj){
	return (obj != undefined);
};

function $each(iterable, fn, bind){
	var type = $type(iterable);
	((type == 'arguments' || type == 'collection' || type == 'array') ? Array : Hash).each(iterable, fn, bind);
};

function $empty(){};

function $extend(original, extended){
	for (var key in (extended || {})) original[key] = extended[key];
	return original;
};

function $H(object){
	return new Hash(object);
};

function $lambda(value){
	return ($type(value) == 'function') ? value : function(){
		return value;
	};
};

function $merge(){
	var args = Array.slice(arguments);
	args.unshift({});
	return $mixin.apply(null, args);
};

function $mixin(mix){
	for (var i = 1, l = arguments.length; i < l; i++){
		var object = arguments[i];
		if ($type(object) != 'object') continue;
		for (var key in object){
			var op = object[key], mp = mix[key];
			mix[key] = (mp && $type(op) == 'object' && $type(mp) == 'object') ? $mixin(mp, op) : $unlink(op);
		}
	}
	return mix;
};

function $pick(){
	for (var i = 0, l = arguments.length; i < l; i++){
		if (arguments[i] != undefined) return arguments[i];
	}
	return null;
};

function $random(min, max){
	return Math.floor(Math.random() * (max - min + 1) + min);
};

function $splat(obj){
	var type = $type(obj);
	return (type) ? ((type != 'array' && type != 'arguments') ? [obj] : obj) : [];
};

var $time = Date.now || function(){
	return +new Date;
};

function $try(){
	for (var i = 0, l = arguments.length; i < l; i++){
		try {
			return arguments[i]();
		} catch(e){}
	}
	return null;
};

function $type(obj){
	if (obj == undefined) return false;
	if (obj.$family) return (obj.$family.name == 'number' && !isFinite(obj)) ? false : obj.$family.name;
	if (obj.nodeName){
		switch (obj.nodeType){
			case 1: return 'element';
			case 3: return (/\S/).test(obj.nodeValue) ? 'textnode' : 'whitespace';
		}
	} else if (typeof obj.length == 'number'){
		if (obj.callee) return 'arguments';
		else if (obj.item) return 'collection';
	}
	return typeof obj;
};

function $unlink(object){
	var unlinked;
	switch ($type(object)){
		case 'object':
			unlinked = {};
			for (var p in object) unlinked[p] = $unlink(object[p]);
		break;
		case 'hash':
			unlinked = new Hash(object);
		break;
		case 'array':
			unlinked = [];
			for (var i = 0, l = object.length; i < l; i++) unlinked[i] = $unlink(object[i]);
		break;
		default: return object;
	}
	return unlinked;
};

/*
---

script: Browser.js

description: The Browser Core. Contains Browser initialization, Window and Document, and the Browser Hash.

license: MIT-style license.

requires: 
- /Native
- /$util

provides: [Browser, Window, Document, $exec]

...
*/

var Browser = $merge({

	Engine: {name: 'unknown', version: 0},

	Platform: {name: (window.orientation != undefined) ? 'ipod' : (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase()},

	Features: {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)},

	Plugins: {},

	Engines: {

		presto: function(){
			return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925));
		},

		/* reports Safari as using trident (IE engine)
		trident: function(){
			return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
		},*/

		webkit: function(){
			if (window.mozInnerScreenX != undefined) return false; // FF9 returns webkit = true!!
			return (navigator.taintEnabled) ? false : ((Browser.Features.xpath) ? ((Browser.Features.query) ? 525 : 420) : 419);
		},

		gecko: function(){
			return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18);
		}

	}

}, Browser || {});

Browser.Platform[Browser.Platform.name] = true;

Browser.detect = function(){

	for (var engine in this.Engines){
		var version = this.Engines[engine]();
		if (version){
			this.Engine = {name: engine, version: version};
			this.Engine[engine] = this.Engine[engine + version] = true;
			break;
		}
	}

	return {name: engine, version: version};

};

Browser.detect();

Browser.Request = function(){
	return $try(function(){
		return new XMLHttpRequest();
	}, function(){
		return new ActiveXObject('MSXML2.XMLHTTP');
	}, function(){
		return new ActiveXObject('Microsoft.XMLHTTP');
	});
};

Browser.Features.xhr = !!(Browser.Request());

Browser.Plugins.Flash = (function(){
	var version = ($try(function(){
		return navigator.plugins['Shockwave Flash'].description;
	}, function(){
		return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
	}) || '0 r0').match(/\d+/g);
	return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0};
})();

function $exec(text){
	if (!text) return text;
	if (window.execScript){
		window.execScript(text);
	} else {
		var script = document.createElement('script');
		script.setAttribute('type', 'text/javascript');
		script[(Browser.Engine.webkit && Browser.Engine.version < 420) ? 'innerText' : 'text'] = text;
		document.head.appendChild(script);
		document.head.removeChild(script);
	}
	return text;
};

Native.UID = 1;

var $uid = (Browser.Engine.trident) ? function(item){
	return (item.uid || (item.uid = [Native.UID++]))[0];
} : function(item){
	return item.uid || (item.uid = Native.UID++);
};

var Window = new Native({

	name: 'Window',

	legacy: (Browser.Engine.trident) ? null: window.Window,

	initialize: function(win){
		$uid(win);
		if (!win.Element){
			win.Element = $empty;
			if (Browser.Engine.webkit) win.document.createElement("iframe"); //fixes safari 2
			win.Element.prototype = (Browser.Engine.webkit) ? window["[[DOMElement.prototype]]"] : {};
		}
		win.document.window = win;
		return $extend(win, Window.Prototype);
	},

	afterImplement: function(property, value){
		window[property] = Window.Prototype[property] = value;
	}

});

Window.Prototype = {$family: {name: 'window'}};

new Window(window);

var Document = new Native({

	name: 'Document',

	legacy: (Browser.Engine.trident) ? null: window.Document,

	initialize: function(doc){
		$uid(doc);
		doc.head = doc.getElementsByTagName('head')[0];
		doc.html = doc.getElementsByTagName('html')[0];
		if (Browser.Engine.trident && Browser.Engine.version <= 4) $try(function(){
			doc.execCommand("BackgroundImageCache", false, true);
		});
		if (Browser.Engine.trident) doc.window.attachEvent('onunload', function(){
			doc.window.detachEvent('onunload', arguments.callee);
			doc.head = doc.html = doc.window = null;
		});
		return $extend(doc, Document.Prototype);
	},

	afterImplement: function(property, value){
		document[property] = Document.Prototype[property] = value;
	}

});

Document.Prototype = {$family: {name: 'document'}};

new Document(document);

/*
---

script: Array.js

description: Contains Array Prototypes like each, contains, and erase.

license: MIT-style license.

requires:
- /$util
- /Array.each

provides: [Array]

...
*/

Array.implement({

	every: function(fn, bind){
		for (var i = 0, l = this.length; i < l; i++){
			if (!fn.call(bind, this[i], i, this)) return false;
		}
		return true;
	},

	filter: function(fn, bind){
		var results = [];
		for (var i = 0, l = this.length; i < l; i++){
			if (fn.call(bind, this[i], i, this)) results.push(this[i]);
		}
		return results;
	},

	clean: function(){
		return this.filter($defined);
	},

	indexOf: function(item, from){
		var len = this.length;
		for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){
			if (this[i] === item) return i;
		}
		return -1;
	},

	map: function(fn, bind){
		var results = [];
		for (var i = 0, l = this.length; i < l; i++) results[i] = fn.call(bind, this[i], i, this);
		return results;
	},

	some: function(fn, bind){
		for (var i = 0, l = this.length; i < l; i++){
			if (fn.call(bind, this[i], i, this)) return true;
		}
		return false;
	},

	associate: function(keys){
		var obj = {}, length = Math.min(this.length, keys.length);
		for (var i = 0; i < length; i++) obj[keys[i]] = this[i];
		return obj;
	},

	link: function(object){
		var result = {};
		for (var i = 0, l = this.length; i < l; i++){
			for (var key in object){
				if (object[key](this[i])){
					result[key] = this[i];
					delete object[key];
					break;
				}
			}
		}
		return result;
	},

	contains: function(item, from){
		return this.indexOf(item, from) != -1;
	},

	extend: function(array){
		for (var i = 0, j = array.length; i < j; i++) this.push(array[i]);
		return this;
	},
	
	getLast: function(){
		return (this.length) ? this[this.length - 1] : null;
	},

	getRandom: function(){
		return (this.length) ? this[$random(0, this.length - 1)] : null;
	},

	include: function(item){
		if (!this.contains(item)) this.push(item);
		return this;
	},

	combine: function(array){
		for (var i = 0, l = array.length; i < l; i++) this.include(array[i]);
		return this;
	},

	erase: function(item){
		for (var i = this.length; i--; i){
			if (this[i] === item) this.splice(i, 1);
		}
		return this;
	},

	empty: function(){
		this.length = 0;
		return this;
	},

	flatten: function(){
		var array = [];
		for (var i = 0, l = this.length; i < l; i++){
			var type = $type(this[i]);
			if (!type) continue;
			array = array.concat((type == 'array' || type == 'collection' || type == 'arguments') ? Array.flatten(this[i]) : this[i]);
		}
		return array;
	},

	hexToRgb: function(array){
		if (this.length != 3) return null;
		var rgb = this.map(function(value){
			if (value.length == 1) value += value;
			return value.toInt(16);
		});
		return (array) ? rgb : 'rgb(' + rgb + ')';
	},

	rgbToHex: function(array){
		if (this.length < 3) return null;
		if (this.length == 4 && this[3] == 0 && !array) return 'transparent';
		var hex = [];
		for (var i = 0; i < 3; i++){
			var bit = (this[i] - 0).toString(16);
			hex.push((bit.length == 1) ? '0' + bit : bit);
		}
		return (array) ? hex : '#' + hex.join('');
	}

});

/*
---

script: String.js

description: Contains String Prototypes like camelCase, capitalize, test, and toInt.

license: MIT-style license.

requires:
- /Native

provides: [String]

...
*/

String.implement({

	test: function(regex, params){
		return ((typeof regex == 'string') ? new RegExp(regex, params) : regex).test(this);
	},

	contains: function(string, separator){
		return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : this.indexOf(string) > -1;
	},

	trim: function(){
		return this.replace(/^\s+|\s+$/g, '');
	},

	clean: function(){
		return this.replace(/\s+/g, ' ').trim();
	},

	camelCase: function(){
		return this.replace(/-\D/g, function(match){
			return match.charAt(1).toUpperCase();
		});
	},

	hyphenate: function(){
		return this.replace(/[A-Z]/g, function(match){
			return ('-' + match.charAt(0).toLowerCase());
		});
	},

	capitalize: function(){
		return this.replace(/\b[a-z]/g, function(match){
			return match.toUpperCase();
		});
	},

	escapeRegExp: function(){
		return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
	},

	toInt: function(base){
		return parseInt(this, base || 10);
	},

	toFloat: function(){
		return parseFloat(this);
	},

	hexToRgb: function(array){
		var hex = this.match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
		return (hex) ? hex.slice(1).hexToRgb(array) : null;
	},

	rgbToHex: function(array){
		var rgb = this.match(/\d{1,3}/g);
		return (rgb) ? rgb.rgbToHex(array) : null;
	},

	stripScripts: function(option){
		var scripts = '';
		var text = this.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, function(){
			scripts += arguments[1] + '\n';
			return '';
		});
		if (option === true) $exec(scripts);
		else if ($type(option) == 'function') option(scripts, text);
		return text;
	},

	substitute: function(object, regexp){
		return this.replace(regexp || (/\\?\{([^{}]+)\}/g), function(match, name){
			if (match.charAt(0) == '\\') return match.slice(1);
			return (object[name] != undefined) ? object[name] : '';
		});
	}

});

/*
---

script: Function.js

description: Contains Function Prototypes like create, bind, pass, and delay.

license: MIT-style license.

requires:
- /Native
- /$util

provides: [Function]

...
*/

Function.implement({

	extend: function(properties){
		for (var property in properties) this[property] = properties[property];
		return this;
	},

	create: function(options){
		var self = this;
		options = options || {};
		return function(event){
			var args = options.arguments;
			args = (args != undefined) ? $splat(args) : Array.slice(arguments, (options.event) ? 1 : 0);
			if (options.event) args = [event || window.event].extend(args);
			var returns = function(){
				return self.apply(options.bind || null, args);
			};
			if (options.delay) return setTimeout(returns, options.delay);
			if (options.periodical) return setInterval(returns, options.periodical);
			if (options.attempt) return $try(returns);
			return returns();
		};
	},

	run: function(args, bind){
		return this.apply(bind, $splat(args));
	},

	pass: function(args, bind){
		return this.create({bind: bind, arguments: args});
	},

	bind: function(bind, args){
		return this.create({bind: bind, arguments: args});
	},

	bindWithEvent: function(bind, args){
		return this.create({bind: bind, arguments: args, event: true});
	},

	attempt: function(args, bind){
		return this.create({bind: bind, arguments: args, attempt: true})();
	},

	delay: function(delay, bind, args){
		return this.create({bind: bind, arguments: args, delay: delay})();
	},

	periodical: function(periodical, bind, args){
		return this.create({bind: bind, arguments: args, periodical: periodical})();
	}

});

/*
---

script: Number.js

description: Contains Number Prototypes like limit, round, times, and ceil.

license: MIT-style license.

requires:
- /Native
- /$util

provides: [Number]

...
*/

Number.implement({

	limit: function(min, max){
		return Math.min(max, Math.max(min, this));
	},

	round: function(precision){
		precision = Math.pow(10, precision || 0);
		return Math.round(this * precision) / precision;
	},

	times: function(fn, bind){
		for (var i = 0; i < this; i++) fn.call(bind, i, this);
	},

	toFloat: function(){
		return parseFloat(this);
	},

	toInt: function(base){
		return parseInt(this, base || 10);
	}

});

Number.alias('times', 'each');

(function(math){
	var methods = {};
	math.each(function(name){
		if (!Number[name]) methods[name] = function(){
			return Math[name].apply(null, [this].concat($A(arguments)));
		};
	});
	Number.implement(methods);
})(['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'sin', 'sqrt', 'tan']);

/*
---

script: Hash.js

description: Contains Hash Prototypes. Provides a means for overcoming the JavaScript practical impossibility of extending native Objects.

license: MIT-style license.

requires:
- /Hash.base

provides: [Hash]

...
*/

Hash.implement({

	has: Object.prototype.hasOwnProperty,

	keyOf: function(value){
		for (var key in this){
			if (this.hasOwnProperty(key) && this[key] === value) return key;
		}
		return null;
	},

	hasValue: function(value){
		return (Hash.keyOf(this, value) !== null);
	},

	extend: function(properties){
		Hash.each(properties || {}, function(value, key){
			Hash.set(this, key, value);
		}, this);
		return this;
	},

	combine: function(properties){
		Hash.each(properties || {}, function(value, key){
			Hash.include(this, key, value);
		}, this);
		return this;
	},

	erase: function(key){
		if (this.hasOwnProperty(key)) delete this[key];
		return this;
	},

	get: function(key){
		return (this.hasOwnProperty(key)) ? this[key] : null;
	},

	set: function(key, value){
		if (!this[key] || this.hasOwnProperty(key)) this[key] = value;
		return this;
	},

	empty: function(){
		Hash.each(this, function(value, key){
			delete this[key];
		}, this);
		return this;
	},

	include: function(key, value){
		if (this[key] == undefined) this[key] = value;
		return this;
	},

	map: function(fn, bind){
		var results = new Hash;
		Hash.each(this, function(value, key){
			results.set(key, fn.call(bind, value, key, this));
		}, this);
		return results;
	},

	filter: function(fn, bind){
		var results = new Hash;
		Hash.each(this, function(value, key){
			if (fn.call(bind, value, key, this)) results.set(key, value);
		}, this);
		return results;
	},

	every: function(fn, bind){
		for (var key in this){
			if (this.hasOwnProperty(key) && !fn.call(bind, this[key], key)) return false;
		}
		return true;
	},

	some: function(fn, bind){
		for (var key in this){
			if (this.hasOwnProperty(key) && fn.call(bind, this[key], key)) return true;
		}
		return false;
	},

	getKeys: function(){
		var keys = [];
		Hash.each(this, function(value, key){
			keys.push(key);
		});
		return keys;
	},

	getValues: function(){
		var values = [];
		Hash.each(this, function(value){
			values.push(value);
		});
		return values;
	},

	toQueryString: function(base){
		var queryString = [];
		Hash.each(this, function(value, key){
			if (base) key = base + '[' + key + ']';
			var result;
			switch ($type(value)){
				case 'object': result = Hash.toQueryString(value, key); break;
				case 'array':
					var qs = {};
					value.each(function(val, i){
						qs[i] = val;
					});
					result = Hash.toQueryString(qs, key);
				break;
				default: result = key + '=' + encodeURIComponent(value);
			}
			if (value != undefined) queryString.push(result);
		});

		return queryString.join('&');
	}

});

Hash.alias({keyOf: 'indexOf', hasValue: 'contains'});

/*
---

script: Element.js

description: One of the most important items in MooTools. Contains the dollar function, the dollars function, and an handful of cross-browser, time-saver methods to let you easily work with HTML Elements.

license: MIT-style license.

requires:
- /Window
- /Document
- /Array
- /String
- /Function
- /Number
- /Hash

provides: [Element, Elements, $, $$, Iframe]

...
*/

var Element = new Native({

	name: 'Element',

	legacy: window.Element,

	initialize: function(tag, props){
		var konstructor = Element.Constructors.get(tag);
		if (konstructor) return konstructor(props);
		if (typeof tag == 'string') return document.newElement(tag, props);
		return document.id(tag).set(props);
	},

	afterImplement: function(key, value){
		Element.Prototype[key] = value;
		if (Array[key]) return;
		Elements.implement(key, function(){
			var items = [], elements = true;
			for (var i = 0, j = this.length; i < j; i++){
				var returns = this[i][key].apply(this[i], arguments);
				items.push(returns);
				if (elements) elements = ($type(returns) == 'element');
			}
			return (elements) ? new Elements(items) : items;
		});
	}

});

Element.Prototype = {$family: {name: 'element'}};

Element.Constructors = new Hash;

var IFrame = new Native({

	name: 'IFrame',

	generics: false,

	initialize: function(){
		var params = Array.link(arguments, {properties: Object.type, iframe: $defined});
		var props = params.properties || {};
		var iframe = document.id(params.iframe);
		var onload = props.onload || $empty;
		delete props.onload;
		props.id = props.name = $pick(props.id, props.name, iframe ? (iframe.id || iframe.name) : 'IFrame_' + $time());
		iframe = new Element(iframe || 'iframe', props);
		var onFrameLoad = function(){
			var host = $try(function(){
				return iframe.contentWindow.location.host;
			});
			if (!host || host == window.location.host){
				var win = new Window(iframe.contentWindow);
				new Document(iframe.contentWindow.document);
				if(!win.Element.prototype) win.Element.prototype = {};
				$extend(win.Element.prototype, Element.Prototype);
			}
			onload.call(iframe.contentWindow, iframe.contentWindow.document);
		};
		var contentWindow = $try(function(){
			return iframe.contentWindow;
		});
		((contentWindow && contentWindow.document.body) || window.frames[props.id]) ? onFrameLoad() : iframe.addListener('load', onFrameLoad);
		return iframe;
	}

});

var Elements = new Native({

	initialize: function(elements, options){
		options = $extend({ddup: true, cash: true}, options);
		elements = elements || [];
		if (options.ddup || options.cash){
			var uniques = {}, returned = [];
			for (var i = 0, l = elements.length; i < l; i++){
				var el = document.id(elements[i], !options.cash);
				if (options.ddup){
					if (uniques[el.uid]) continue;
					uniques[el.uid] = true;
				}
				if (el) returned.push(el);
			}
			elements = returned;
		}
		return (options.cash) ? $extend(elements, this) : elements;
	}

});

Elements.implement({

	filter: function(filter, bind){
		if (!filter) return this;
		return new Elements(Array.filter(this, (typeof filter == 'string') ? function(item){
			return item.match(filter);
		} : filter, bind));
	}

});

Document.implement({

	newElement: function(tag, props){
		if (Browser.Engine.trident && props){
			['name', 'type', 'checked'].each(function(attribute){
				if (!props[attribute]) return;
				tag += ' ' + attribute + '="' + props[attribute] + '"';
				if (attribute != 'checked') delete props[attribute];
			});
			tag = '<' + tag + '>';
		}
		return document.id(this.createElement(tag)).set(props);
	},

	newTextNode: function(text){
		return this.createTextNode(text);
	},

	getDocument: function(){
		return this;
	},

	getWindow: function(){
		return this.window;
	},
	
	id: (function(){
		
		var types = {

			string: function(id, nocash, doc){
				id = doc.getElementById(id);
				return (id) ? types.element(id, nocash) : null;
			},
			
			element: function(el, nocash){
				$uid(el);
				if (!nocash && !el.$family && !(/^object|embed$/i).test(el.tagName)){
					var proto = Element.Prototype;
					for (var p in proto) el[p] = proto[p];
				};
				return el;
			},
			
			object: function(obj, nocash, doc){
				if (obj.toElement) return types.element(obj.toElement(doc), nocash);
				return null;
			}
			
		};

		types.textnode = types.whitespace = types.window = types.document = $arguments(0);
		
		return function(el, nocash, doc){
			if (el && el.$family && el.uid) return el;
			var type = $type(el);
			return (types[type]) ? types[type](el, nocash, doc || document) : null;
		};

	})()

});

if (window.$ == null) Window.implement({
	$: function(el, nc){
		return document.id(el, nc, this.document);
	}
});

Window.implement({

	$$: function(selector){
		if (arguments.length == 1 && typeof selector == 'string') return this.document.getElements(selector);
		var elements = [];
		var args = Array.flatten(arguments);
		for (var i = 0, l = args.length; i < l; i++){
			var item = args[i];
			switch ($type(item)){
				case 'element': elements.push(item); break;
				case 'string': elements.extend(this.document.getElements(item, true));
			}
		}
		return new Elements(elements);
	},

	getDocument: function(){
		return this.document;
	},

	getWindow: function(){
		return this;
	}

});

Native.implement([Element, Document], {

	getElement: function(selector, nocash){
		return document.id(this.getElements(selector, true)[0] || null, nocash);
	},

	getElements: function(tags, nocash){
		tags = tags.split(',');
		var elements = [];
		var ddup = (tags.length > 1);
		tags.each(function(tag){
			var partial = this.getElementsByTagName(tag.trim());
			(ddup) ? elements.extend(partial) : elements = partial;
		}, this);
		return new Elements(elements, {ddup: ddup, cash: !nocash});
	}

});

(function(){

var collected = {}, storage = {};
var props = {input: 'checked', option: 'selected', textarea: (Browser.Engine.webkit && Browser.Engine.version < 420) ? 'innerHTML' : 'value'};

var get = function(uid){
	return (storage[uid] || (storage[uid] = {}));
};

var clean = function(item, retain){
	if (!item) return;
	var uid = item.uid;
	if (Browser.Engine.trident){
		if (item.clearAttributes){
			var clone = retain && item.cloneNode(false);
			item.clearAttributes();
			if (clone) item.mergeAttributes(clone);
		} else if (item.removeEvents){
			item.removeEvents();
		}
		if ((/object/i).test(item.tagName)){
			for (var p in item){
				if (typeof item[p] == 'function') item[p] = $empty;
			}
			Element.dispose(item);
		}
	}	
	if (!uid) return;
	collected[uid] = storage[uid] = null;
};

var purge = function(){
	Hash.each(collected, clean);
	if (Browser.Engine.trident) $A(document.getElementsByTagName('object')).each(clean);
	if (window.CollectGarbage) CollectGarbage();
	collected = storage = null;
};

var walk = function(element, walk, start, match, all, nocash){
	var el = element[start || walk];
	var elements = [];
	while (el){
		if (el.nodeType == 1 && (!match || Element.match(el, match))){
			if (!all) return document.id(el, nocash);
			elements.push(el);
		}
		el = el[walk];
	}
	return (all) ? new Elements(elements, {ddup: false, cash: !nocash}) : null;
};

var attributes = {
	'html': 'innerHTML',
	'class': 'className',
	'for': 'htmlFor',
	'defaultValue': 'defaultValue',
	'text': (Browser.Engine.trident || (Browser.Engine.webkit && Browser.Engine.version < 420)) ? 'innerText' : 'textContent'
};
var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', 'disabled', 'readonly', 'multiple', 'selected', 'noresize', 'defer'];
var camels = ['value', 'type', 'defaultValue', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', 'frameBorder', 'maxLength', 'readOnly', 'rowSpan', 'tabIndex', 'useMap'];

bools = bools.associate(bools);

Hash.extend(attributes, bools);
Hash.extend(attributes, camels.associate(camels.map(String.toLowerCase)));

var inserters = {

	before: function(context, element){
		if (element.parentNode) element.parentNode.insertBefore(context, element);
	},

	after: function(context, element){
		if (!element.parentNode) return;
		var next = element.nextSibling;
		(next) ? element.parentNode.insertBefore(context, next) : element.parentNode.appendChild(context);
	},

	bottom: function(context, element){
		element.appendChild(context);
	},

	top: function(context, element){
		var first = element.firstChild;
		(first) ? element.insertBefore(context, first) : element.appendChild(context);
	}

};

inserters.inside = inserters.bottom;

Hash.each(inserters, function(inserter, where){

	where = where.capitalize();

	Element.implement('inject' + where, function(el){
		inserter(this, document.id(el, true));
		return this;
	});

	Element.implement('grab' + where, function(el){
		inserter(document.id(el, true), this);
		return this;
	});

});

Element.implement({

	set: function(prop, value){
		switch ($type(prop)){
			case 'object':
				for (var p in prop) this.set(p, prop[p]);
				break;
			case 'string':
				var property = Element.Properties.get(prop);
				(property && property.set) ? property.set.apply(this, Array.slice(arguments, 1)) : this.setProperty(prop, value);
		}
		return this;
	},

	get: function(prop){
		var property = Element.Properties.get(prop);
		return (property && property.get) ? property.get.apply(this, Array.slice(arguments, 1)) : this.getProperty(prop);
	},

	erase: function(prop){
		var property = Element.Properties.get(prop);
		(property && property.erase) ? property.erase.apply(this) : this.removeProperty(prop);
		return this;
	},

	setProperty: function(attribute, value){
		var key = attributes[attribute];
		if (value == undefined) return this.removeProperty(attribute);
		if (key && bools[attribute]) value = !!value;
		(key) ? this[key] = value : this.setAttribute(attribute, '' + value);
		return this;
	},

	setProperties: function(attributes){
		for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
		return this;
	},

	getProperty: function(attribute){
		var key = attributes[attribute];
		var value = (key) ? this[key] : this.getAttribute(attribute, 2);
		return (bools[attribute]) ? !!value : (key) ? value : value || null;
	},

	getProperties: function(){
		var args = $A(arguments);
		return args.map(this.getProperty, this).associate(args);
	},

	removeProperty: function(attribute){
		var key = attributes[attribute];
		(key) ? this[key] = (key && bools[attribute]) ? false : '' : this.removeAttribute(attribute);
		return this;
	},

	removeProperties: function(){
		Array.each(arguments, this.removeProperty, this);
		return this;
	},

	hasClass: function(className){
		return this.className.contains(className, ' ');
	},

	addClass: function(className){
		if (!this.hasClass(className)) this.className = (this.className + ' ' + className).clean();
		return this;
	},

	removeClass: function(className){
		this.className = this.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1');
		return this;
	},

	toggleClass: function(className){
		return this.hasClass(className) ? this.removeClass(className) : this.addClass(className);
	},

	adopt: function(){
		Array.flatten(arguments).each(function(element){
			element = document.id(element, true);
			if (element) this.appendChild(element);
		}, this);
		return this;
	},

	appendText: function(text, where){
		return this.grab(this.getDocument().newTextNode(text), where);
	},

	grab: function(el, where){
		inserters[where || 'bottom'](document.id(el, true), this);
		return this;
	},

	inject: function(el, where){
		inserters[where || 'bottom'](this, document.id(el, true));
		return this;
	},

	replaces: function(el){
		el = document.id(el, true);
		el.parentNode.replaceChild(this, el);
		return this;
	},

	wraps: function(el, where){
		el = document.id(el, true);
		return this.replaces(el).grab(el, where);
	},

	getPrevious: function(match, nocash){
		return walk(this, 'previousSibling', null, match, false, nocash);
	},

	getAllPrevious: function(match, nocash){
		return walk(this, 'previousSibling', null, match, true, nocash);
	},

	getNext: function(match, nocash){
		return walk(this, 'nextSibling', null, match, false, nocash);
	},

	getAllNext: function(match, nocash){
		return walk(this, 'nextSibling', null, match, true, nocash);
	},

	getFirst: function(match, nocash){
		return walk(this, 'nextSibling', 'firstChild', match, false, nocash);
	},

	getLast: function(match, nocash){
		return walk(this, 'previousSibling', 'lastChild', match, false, nocash);
	},

	getParent: function(match, nocash){
		return walk(this, 'parentNode', null, match, false, nocash);
	},

	getParents: function(match, nocash){
		return walk(this, 'parentNode', null, match, true, nocash);
	},
	
	getSiblings: function(match, nocash){
		return this.getParent().getChildren(match, nocash).erase(this);
	},

	getChildren: function(match, nocash){
		return walk(this, 'nextSibling', 'firstChild', match, true, nocash);
	},

	getWindow: function(){
		return this.ownerDocument.window;
	},

	getDocument: function(){
		return this.ownerDocument;
	},

	getElementById: function(id, nocash){
		var el = this.ownerDocument.getElementById(id);
		if (!el) return null;
		for (var parent = el.parentNode; parent != this; parent = parent.parentNode){
			if (!parent) return null;
		}
		return document.id(el, nocash);
	},

	getSelected: function(){
		return new Elements($A(this.options).filter(function(option){
			return option.selected;
		}));
	},

	getComputedStyle: function(property){
		if (this.currentStyle) return this.currentStyle[property.camelCase()];
		var computed = this.getDocument().defaultView.getComputedStyle(this, null);
		return (computed) ? computed.getPropertyValue([property.hyphenate()]) : null;
	},

	toQueryString: function(){
		var queryString = [];
		this.getElements('input, select, textarea', true).each(function(el){
			if (!el.name || el.disabled || el.type == 'submit' || el.type == 'reset' || el.type == 'file') return;
			var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
				return opt.value;
			}) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;
			$splat(value).each(function(val){
				if (typeof val != 'undefined') queryString.push(el.name + '=' + encodeURIComponent(val));
			});
		});
		return queryString.join('&');
	},

	clone: function(contents, keepid){
		contents = contents !== false;
		var clone = this.cloneNode(contents);
		var clean = function(node, element){
			if (!keepid) node.removeAttribute('id');
			if (Browser.Engine.trident){
				node.clearAttributes();
				node.mergeAttributes(element);
				node.removeAttribute('uid');
				if (node.options){
					var no = node.options, eo = element.options;
					for (var j = no.length; j--;) no[j].selected = eo[j].selected;
				}
			}
			var prop = props[element.tagName.toLowerCase()];
			if (prop && element[prop]) node[prop] = element[prop];
		};

		if (contents){
			var ce = clone.getElementsByTagName('*'), te = this.getElementsByTagName('*');
			for (var i = ce.length; i--;) clean(ce[i], te[i]);
		}

		clean(clone, this);
		return document.id(clone);
	},

	destroy: function(){
		Element.empty(this);
		Element.dispose(this);
		clean(this, true);
		return null;
	},

	empty: function(){
		$A(this.childNodes).each(function(node){
			Element.destroy(node);
		});
		return this;
	},

	dispose: function(){
		return (this.parentNode) ? this.parentNode.removeChild(this) : this;
	},

	hasChild: function(el){
		el = document.id(el, true);
		if (!el) return false;
		if (Browser.Engine.webkit && Browser.Engine.version < 420) return $A(this.getElementsByTagName(el.tagName)).contains(el);
		return (this.contains) ? (this != el && this.contains(el)) : !!(this.compareDocumentPosition(el) & 16);
	},

	match: function(tag){
		return (!tag || (tag == this) || (Element.get(this, 'tag') == tag));
	}

});

Native.implement([Element, Window, Document], {

	addListener: function(type, fn){
		if (type == 'unload'){
			var old = fn, self = this;
			fn = function(){
				self.removeListener('unload', fn);
				old();
			};
		} else {
			collected[this.uid] = this;
		}
		if (this.addEventListener) this.addEventListener(type, fn, false);
		else this.attachEvent('on' + type, fn);
		return this;
	},

	removeListener: function(type, fn){
		if (this.removeEventListener) this.removeEventListener(type, fn, false);
		else this.detachEvent('on' + type, fn);
		return this;
	},

	retrieve: function(property, dflt){
		var storage = get(this.uid), prop = storage[property];
		if (dflt != undefined && prop == undefined) prop = storage[property] = dflt;
		return $pick(prop);
	},

	store: function(property, value){
		var storage = get(this.uid);
		storage[property] = value;
		return this;
	},

	eliminate: function(property){
		var storage = get(this.uid);
		delete storage[property];
		return this;
	}

});

window.addListener('unload', purge);

})();

Element.Properties = new Hash;

Element.Properties.style = {

	set: function(style){
		this.style.cssText = style;
	},

	get: function(){
		return this.style.cssText;
	},

	erase: function(){
		this.style.cssText = '';
	}

};

Element.Properties.tag = {

	get: function(){
		return this.tagName.toLowerCase();
	}

};

Element.Properties.html = (function(){
	var wrapper = document.createElement('div');

	var translations = {
		table: [1, '<table>', '</table>'],
		select: [1, '<select>', '</select>'],
		tbody: [2, '<table><tbody>', '</tbody></table>'],
		tr: [3, '<table><tbody><tr>', '</tr></tbody></table>']
	};
	translations.thead = translations.tfoot = translations.tbody;

	var html = {
		set: function(){
			var html = Array.flatten(arguments).join('');
			var wrap = Browser.Engine.trident && translations[this.get('tag')];
			if (wrap){
				var first = wrapper;
				first.innerHTML = wrap[1] + html + wrap[2];
				for (var i = wrap[0]; i--;) first = first.firstChild;
				this.empty().adopt(first.childNodes);
			} else {
				this.innerHTML = html;
			}
		}
	};

	html.erase = html.set;

	return html;
})();

if (Browser.Engine.webkit && Browser.Engine.version < 420) Element.Properties.text = {
	get: function(){
		if (this.innerText) return this.innerText;
		var temp = this.ownerDocument.newElement('div', {html: this.innerHTML}).inject(this.ownerDocument.body);
		var text = temp.innerText;
		temp.destroy();
		return text;
	}
};

/*
---

script: Element.Style.js

description: Contains methods for interacting with the styles of Elements in a fashionable way.

license: MIT-style license.

requires:
- /Element

provides: [Element.Style]

...
*/

Element.Properties.styles = {set: function(styles){
	this.setStyles(styles);
}};

Element.Properties.opacity = {

	set: function(opacity, novisibility){
		if (!novisibility){
			if (opacity == 0){
				if (this.style.visibility != 'hidden') this.style.visibility = 'hidden';
			} else {
				if (this.style.visibility != 'visible') this.style.visibility = 'visible';
			}
		}
		if (!this.currentStyle || !this.currentStyle.hasLayout) this.style.zoom = 1;
		if (Browser.Engine.trident) this.style.filter = (opacity == 1) ? '' : 'alpha(opacity=' + opacity * 100 + ')';
		this.style.opacity = opacity;
		this.store('opacity', opacity);
	},

	get: function(){
		return this.retrieve('opacity', 1);
	}

};

Element.implement({

	setOpacity: function(value){
		return this.set('opacity', value, true);
	},

	getOpacity: function(){
		return this.get('opacity');
	},

	setStyle: function(property, value){
		switch (property){
			case 'opacity': return this.set('opacity', parseFloat(value));
			case 'float': property = (Browser.Engine.trident) ? 'styleFloat' : 'cssFloat';
		}
		property = property.camelCase();
		if ($type(value) != 'string'){
			var map = (Element.Styles.get(property) || '@').split(' ');
			value = $splat(value).map(function(val, i){
				if (!map[i]) return '';
				return ($type(val) == 'number') ? map[i].replace('@', Math.round(val)) : val;
			}).join(' ');
		} else if (value == String(Number(value))){
			value = Math.round(value);
		}
		this.style[property] = value;
		return this;
	},

	getStyle: function(property){
		switch (property){
			case 'opacity': return this.get('opacity');
			case 'float': property = (Browser.Engine.trident) ? 'styleFloat' : 'cssFloat';
		}
		property = property.camelCase();
		var result = this.style[property];
		if (!$chk(result)){
			result = [];
			for (var style in Element.ShortStyles){
				if (property != style) continue;
				for (var s in Element.ShortStyles[style]) result.push(this.getStyle(s));
				return result.join(' ');
			}
			result = this.getComputedStyle(property);
		}
		if (result){
			result = String(result);
			var color = result.match(/rgba?\([\d\s,]+\)/);
			if (color) result = result.replace(color[0], color[0].rgbToHex());
		}
		if (Browser.Engine.presto || (Browser.Engine.trident && !$chk(parseInt(result, 10)))){
			if (property.test(/^(height|width)$/)){
				var values = (property == 'width') ? ['left', 'right'] : ['top', 'bottom'], size = 0;
				values.each(function(value){
					size += this.getStyle('border-' + value + '-width').toInt() + this.getStyle('padding-' + value).toInt();
				}, this);
				return this['offset' + property.capitalize()] - size + 'px';
			}
			if ((Browser.Engine.presto) && String(result).test('px')) return result;
			if (property.test(/(border(.+)Width|margin|padding)/)) return '0px';
		}
		return result;
	},

	setStyles: function(styles){
		for (var style in styles) this.setStyle(style, styles[style]);
		return this;
	},

	getStyles: function(){
		var result = {};
		Array.flatten(arguments).each(function(key){
			result[key] = this.getStyle(key);
		}, this);
		return result;
	}

});

Element.Styles = new Hash({
	left: '@px', top: '@px', bottom: '@px', right: '@px',
	width: '@px', height: '@px', maxWidth: '@px', maxHeight: '@px', minWidth: '@px', minHeight: '@px',
	backgroundColor: 'rgb(@, @, @)', backgroundPosition: '@px @px', color: 'rgb(@, @, @)',
	fontSize: '@px', letterSpacing: '@px', lineHeight: '@px', clip: 'rect(@px @px @px @px)',
	margin: '@px @px @px @px', padding: '@px @px @px @px', border: '@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)',
	borderWidth: '@px @px @px @px', borderStyle: '@ @ @ @', borderColor: 'rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)',
	zIndex: '@', 'zoom': '@', fontWeight: '@', textIndent: '@px', opacity: '@'
});

Element.ShortStyles = {margin: {}, padding: {}, border: {}, borderWidth: {}, borderStyle: {}, borderColor: {}};

['Top', 'Right', 'Bottom', 'Left'].each(function(direction){
	var Short = Element.ShortStyles;
	var All = Element.Styles;
	['margin', 'padding'].each(function(style){
		var sd = style + direction;
		Short[style][sd] = All[sd] = '@px';
	});
	var bd = 'border' + direction;
	Short.border[bd] = All[bd] = '@px @ rgb(@, @, @)';
	var bdw = bd + 'Width', bds = bd + 'Style', bdc = bd + 'Color';
	Short[bd] = {};
	Short.borderWidth[bdw] = Short[bd][bdw] = All[bdw] = '@px';
	Short.borderStyle[bds] = Short[bd][bds] = All[bds] = '@';
	Short.borderColor[bdc] = Short[bd][bdc] = All[bdc] = 'rgb(@, @, @)';
});

/*
---

script: Element.Dimensions.js

description: Contains methods to work with size, scroll, or positioning of Elements and the window object.

license: MIT-style license.

credits:
- Element positioning based on the [qooxdoo](http://qooxdoo.org/) code and smart browser fixes, [LGPL License](http://www.gnu.org/licenses/lgpl.html).
- Viewport dimensions based on [YUI](http://developer.yahoo.com/yui/) code, [BSD License](http://developer.yahoo.com/yui/license.html).

requires:
- /Element

provides: [Element.Dimensions]

...
*/

(function(){

Element.implement({

	scrollTo: function(x, y){
		if (isBody(this)){
			this.getWindow().scrollTo(x, y);
		} else {
			this.scrollLeft = x;
			this.scrollTop = y;
		}
		return this;
	},

	getSize: function(){
		if (isBody(this)) return this.getWindow().getSize();
		return {x: this.offsetWidth, y: this.offsetHeight};
	},

	getScrollSize: function(){
		if (isBody(this)) return this.getWindow().getScrollSize();
		return {x: this.scrollWidth, y: this.scrollHeight};
	},

	getScroll: function(){
		if (isBody(this)) return this.getWindow().getScroll();
		return {x: this.scrollLeft, y: this.scrollTop};
	},

	getScrolls: function(){
		var element = this, position = {x: 0, y: 0};
		while (element && !isBody(element)){
			position.x += element.scrollLeft;
			position.y += element.scrollTop;
			element = element.parentNode;
		}
		return position;
	},

	getOffsetParent: function(){
		var element = this;
		if (isBody(element)) return null;
		if (!Browser.Engine.trident) return element.offsetParent;
		while ((element = element.parentNode) && !isBody(element)){
			if (styleString(element, 'position') != 'static') return element;
		}
		return null;
	},

	getOffsets: function(){
		if (this.getBoundingClientRect){
			var bound = this.getBoundingClientRect(),
				html = document.id(this.getDocument().documentElement),
				htmlScroll = html.getScroll(),
				//htmlScroll = { x: 0, y: 0},
				elemScrolls = this.getScrolls(),
				elemScroll = this.getScroll(),
				isFixed = (styleString(this, 'position') == 'fixed');

			return {
				x: bound.left.toInt() + elemScrolls.x - elemScroll.x + ((isFixed) ? 0 : htmlScroll.x) - html.clientLeft,
				y: bound.top.toInt()  + elemScrolls.y - elemScroll.y + ((isFixed) ? 0 : htmlScroll.y) - html.clientTop
			};
		}

		var element = this, position = {x: 0, y: 0};
		if (isBody(this)) return position;

		while (element && !isBody(element)){
			position.x += element.offsetLeft;
			position.y += element.offsetTop;

			if (Browser.Engine.gecko){
				if (!borderBox(element)){
					position.x += leftBorder(element);
					position.y += topBorder(element);
				}
				var parent = element.parentNode;
				if (parent && styleString(parent, 'overflow') != 'visible'){
					position.x += leftBorder(parent);
					position.y += topBorder(parent);
				}
			} else if (element != this && Browser.Engine.webkit){
				position.x += leftBorder(element);
				position.y += topBorder(element);
			}

			element = element.offsetParent;
		}
		if (Browser.Engine.gecko && !borderBox(this)){
			position.x -= leftBorder(this);
			position.y -= topBorder(this);
		}
		return position;
	},

	getPosition: function(relative){
		if (isBody(this)) return {x: 0, y: 0};
		var offset = this.getOffsets(),
				scroll = this.getScrolls();
		var position = {
			x: offset.x - scroll.x,
			y: offset.y - scroll.y
		};
		var relativePosition = (relative && (relative = document.id(relative))) ? relative.getPosition() : {x: 0, y: 0};
		return {x: position.x - relativePosition.x, y: position.y - relativePosition.y};
	},

	getCoordinates: function(element){
		if (isBody(this)) return this.getWindow().getCoordinates();
		var position = this.getPosition(element),
				size = this.getSize();
		var obj = {
			left: position.x,
			top: position.y,
			width: size.x,
			height: size.y
		};
		obj.right = obj.left + obj.width;
		obj.bottom = obj.top + obj.height;
		return obj;
	},

	computePosition: function(obj){
		return {
			left: obj.x - styleNumber(this, 'margin-left'),
			top: obj.y - styleNumber(this, 'margin-top')
		};
	},

	setPosition: function(obj){
		return this.setStyles(this.computePosition(obj));
	}

});


Native.implement([Document, Window], {

	getSize: function(){
		if (Browser.Engine.presto || Browser.Engine.webkit){
			var win = this.getWindow();
			return {x: win.innerWidth, y: win.innerHeight};
		}
		var doc = getCompatElement(this);
		return {x: doc.clientWidth, y: doc.clientHeight};
	},

	getScroll: function(){
		var win = this.getWindow(), doc = getCompatElement(this);
		return {x: win.pageXOffset || doc.scrollLeft, y: win.pageYOffset || doc.scrollTop};
	},

	getScrollSize: function(){
		var doc = getCompatElement(this), min = this.getSize();
		return {x: Math.max(doc.scrollWidth, min.x), y: Math.max(doc.scrollHeight, min.y)};
	},

	getPosition: function(){
		return {x: 0, y: 0};
	},

	getCoordinates: function(){
		var size = this.getSize();
		return {top: 0, left: 0, bottom: size.y, right: size.x, height: size.y, width: size.x};
	}

});

// private methods

var styleString = Element.getComputedStyle;

function styleNumber(element, style){
	return styleString(element, style).toInt() || 0;
};

function borderBox(element){
	return styleString(element, '-moz-box-sizing') == 'border-box';
};

function topBorder(element){
	return styleNumber(element, 'border-top-width');
};

function leftBorder(element){
	return styleNumber(element, 'border-left-width');
};

function isBody(element){
	return (/^(?:body|html)$/i).test(element.tagName);
};

function getCompatElement(element){
	var doc = element.getDocument();
	return (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
};

})();

//aliases
Element.alias('setPosition', 'position'); //compatability

Native.implement([Window, Document, Element], {

	getHeight: function(){
		return this.getSize().y;
	},

	getWidth: function(){
		return this.getSize().x;
	},

	getScrollTop: function(){
		return this.getScroll().y;
	},

	getScrollLeft: function(){
		return this.getScroll().x;
	},

	getScrollHeight: function(){
		return this.getScrollSize().y;
	},

	getScrollWidth: function(){
		return this.getScrollSize().x;
	},

	getTop: function(){
		return this.getPosition().y;
	},

	getLeft: function(){
		return this.getPosition().x;
	}

});

/*
---

script: Selectors.js

description: Adds advanced CSS-style querying capabilities for targeting HTML Elements. Includes pseudo selectors.

license: MIT-style license.

requires:
- /Element

provides: [Selectors]

...
*/

Native.implement([Document, Element], {

	getElements: function(expression, nocash){
		expression = expression.split(',');
		var items, local = {};
		for (var i = 0, l = expression.length; i < l; i++){
			var selector = expression[i], elements = Selectors.Utils.search(this, selector, local);
			if (i != 0 && elements.item) elements = $A(elements);
			items = (i == 0) ? elements : (items.item) ? $A(items).concat(elements) : items.concat(elements);
		}
		return new Elements(items, {ddup: (expression.length > 1), cash: !nocash});
	}

});

Element.implement({

	match: function(selector){
		if (!selector || (selector == this)) return true;
		var tagid = Selectors.Utils.parseTagAndID(selector);
		var tag = tagid[0], id = tagid[1];
		if (!Selectors.Filters.byID(this, id) || !Selectors.Filters.byTag(this, tag)) return false;
		var parsed = Selectors.Utils.parseSelector(selector);
		return (parsed) ? Selectors.Utils.filter(this, parsed, {}) : true;
	}

});

var Selectors = {Cache: {nth: {}, parsed: {}}};

Selectors.RegExps = {
	id: (/#([\w-]+)/),
	tag: (/^(\w+|\*)/),
	quick: (/^(\w+|\*)$/),
	splitter: (/\s*([+>~\s])\s*([a-zA-Z#.*:\[])/g),
	combined: (/\.([\w-]+)|\[(\w+)(?:([!*^$~|]?=)(["']?)([^\4]*?)\4)?\]|:([\w-]+)(?:\(["']?(.*?)?["']?\)|$)/g)
};

Selectors.Utils = {

	chk: function(item, uniques){
		if (!uniques) return true;
		var uid = $uid(item);
		if (!uniques[uid]) return uniques[uid] = true;
		return false;
	},

	parseNthArgument: function(argument){
		if (Selectors.Cache.nth[argument]) return Selectors.Cache.nth[argument];
		var parsed = argument.match(/^([+-]?\d*)?([a-z]+)?([+-]?\d*)?$/);
		if (!parsed) return false;
		var inta = parseInt(parsed[1], 10);
		var a = (inta || inta === 0) ? inta : 1;
		var special = parsed[2] || false;
		var b = parseInt(parsed[3], 10) || 0;
		if (a != 0){
			b--;
			while (b < 1) b += a;
			while (b >= a) b -= a;
		} else {
			a = b;
			special = 'index';
		}
		switch (special){
			case 'n': parsed = {a: a, b: b, special: 'n'}; break;
			case 'odd': parsed = {a: 2, b: 0, special: 'n'}; break;
			case 'even': parsed = {a: 2, b: 1, special: 'n'}; break;
			case 'first': parsed = {a: 0, special: 'index'}; break;
			case 'last': parsed = {special: 'last-child'}; break;
			case 'only': parsed = {special: 'only-child'}; break;
			default: parsed = {a: (a - 1), special: 'index'};
		}

		return Selectors.Cache.nth[argument] = parsed;
	},

	parseSelector: function(selector){
		if (Selectors.Cache.parsed[selector]) return Selectors.Cache.parsed[selector];
		var m, parsed = {classes: [], pseudos: [], attributes: []};
		while ((m = Selectors.RegExps.combined.exec(selector))){
			var cn = m[1], an = m[2], ao = m[3], av = m[5], pn = m[6], pa = m[7];
			if (cn){
				parsed.classes.push(cn);
			} else if (pn){
				var parser = Selectors.Pseudo.get(pn);
				if (parser) parsed.pseudos.push({parser: parser, argument: pa});
				else parsed.attributes.push({name: pn, operator: '=', value: pa});
			} else if (an){
				parsed.attributes.push({name: an, operator: ao, value: av});
			}
		}
		if (!parsed.classes.length) delete parsed.classes;
		if (!parsed.attributes.length) delete parsed.attributes;
		if (!parsed.pseudos.length) delete parsed.pseudos;
		if (!parsed.classes && !parsed.attributes && !parsed.pseudos) parsed = null;
		return Selectors.Cache.parsed[selector] = parsed;
	},

	parseTagAndID: function(selector){
		var tag = selector.match(Selectors.RegExps.tag);
		var id = selector.match(Selectors.RegExps.id);
		return [(tag) ? tag[1] : '*', (id) ? id[1] : false];
	},

	filter: function(item, parsed, local){
		var i;
		if (parsed.classes){
			for (i = parsed.classes.length; i--; i){
				var cn = parsed.classes[i];
				if (!Selectors.Filters.byClass(item, cn)) return false;
			}
		}
		if (parsed.attributes){
			for (i = parsed.attributes.length; i--; i){
				var att = parsed.attributes[i];
				if (!Selectors.Filters.byAttribute(item, att.name, att.operator, att.value)) return false;
			}
		}
		if (parsed.pseudos){
			for (i = parsed.pseudos.length; i--; i){
				var psd = parsed.pseudos[i];
				if (!Selectors.Filters.byPseudo(item, psd.parser, psd.argument, local)) return false;
			}
		}
		return true;
	},

	getByTagAndID: function(ctx, tag, id){
		if (id){
			var item = (ctx.getElementById) ? ctx.getElementById(id, true) : Element.getElementById(ctx, id, true);
			return (item && Selectors.Filters.byTag(item, tag)) ? [item] : [];
		} else {
			return ctx.getElementsByTagName(tag);
		}
	},

	search: function(self, expression, local){
		var splitters = [];

		var selectors = expression.trim().replace(Selectors.RegExps.splitter, function(m0, m1, m2){
			splitters.push(m1);
			return ':)' + m2;
		}).split(':)');

		var items, filtered, item;

		for (var i = 0, l = selectors.length; i < l; i++){

			var selector = selectors[i];

			if (i == 0 && Selectors.RegExps.quick.test(selector)){
				items = self.getElementsByTagName(selector);
				continue;
			}

			var splitter = splitters[i - 1];

			var tagid = Selectors.Utils.parseTagAndID(selector);
			var tag = tagid[0], id = tagid[1];

			if (i == 0){
				items = Selectors.Utils.getByTagAndID(self, tag, id);
			} else {
				var uniques = {}, found = [];
				for (var j = 0, k = items.length; j < k; j++) found = Selectors.Getters[splitter](found, items[j], tag, id, uniques);
				items = found;
			}

			var parsed = Selectors.Utils.parseSelector(selector);

			if (parsed){
				filtered = [];
				for (var m = 0, n = items.length; m < n; m++){
					item = items[m];
					if (Selectors.Utils.filter(item, parsed, local)) filtered.push(item);
				}
				items = filtered;
			}

		}

		return items;

	}

};

Selectors.Getters = {

	' ': function(found, self, tag, id, uniques){
		var items = Selectors.Utils.getByTagAndID(self, tag, id);
		for (var i = 0, l = items.length; i < l; i++){
			var item = items[i];
			if (Selectors.Utils.chk(item, uniques)) found.push(item);
		}
		return found;
	},

	'>': function(found, self, tag, id, uniques){
		var children = Selectors.Utils.getByTagAndID(self, tag, id);
		for (var i = 0, l = children.length; i < l; i++){
			var child = children[i];
			if (child.parentNode == self && Selectors.Utils.chk(child, uniques)) found.push(child);
		}
		return found;
	},

	'+': function(found, self, tag, id, uniques){
		while ((self = self.nextSibling)){
			if (self.nodeType == 1){
				if (Selectors.Utils.chk(self, uniques) && Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
				break;
			}
		}
		return found;
	},

	'~': function(found, self, tag, id, uniques){
		while ((self = self.nextSibling)){
			if (self.nodeType == 1){
				if (!Selectors.Utils.chk(self, uniques)) break;
				if (Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
			}
		}
		return found;
	}

};

Selectors.Filters = {

	byTag: function(self, tag){
		return (tag == '*' || (self.tagName && self.tagName.toLowerCase() == tag));
	},

	byID: function(self, id){
		return (!id || (self.id && self.id == id));
	},

	byClass: function(self, klass){
		return (self.className && self.className.contains && self.className.contains(klass, ' '));
	},

	byPseudo: function(self, parser, argument, local){
		return parser.call(self, argument, local);
	},

	byAttribute: function(self, name, operator, value){
		var result = Element.prototype.getProperty.call(self, name);
		if (!result) return (operator == '!=');
		if (!operator || value == undefined) return true;
		switch (operator){
			case '=': return (result == value);
			case '*=': return (result.contains(value));
			case '^=': return (result.substr(0, value.length) == value);
			case '$=': return (result.substr(result.length - value.length) == value);
			case '!=': return (result != value);
			case '~=': return result.contains(value, ' ');
			case '|=': return result.contains(value, '-');
		}
		return false;
	}

};

Selectors.Pseudo = new Hash({

	// w3c pseudo selectors

	checked: function(){
		return this.checked;
	},
	
	empty: function(){
		return !(this.innerText || this.textContent || '').length;
	},

	not: function(selector){
		return !Element.match(this, selector);
	},

	contains: function(text){
		return (this.innerText || this.textContent || '').contains(text);
	},

	'first-child': function(){
		return Selectors.Pseudo.index.call(this, 0);
	},

	'last-child': function(){
		var element = this;
		while ((element = element.nextSibling)){
			if (element.nodeType == 1) return false;
		}
		return true;
	},

	'only-child': function(){
		var prev = this;
		while ((prev = prev.previousSibling)){
			if (prev.nodeType == 1) return false;
		}
		var next = this;
		while ((next = next.nextSibling)){
			if (next.nodeType == 1) return false;
		}
		return true;
	},

	'nth-child': function(argument, local){
		argument = (argument == undefined) ? 'n' : argument;
		var parsed = Selectors.Utils.parseNthArgument(argument);
		if (parsed.special != 'n') return Selectors.Pseudo[parsed.special].call(this, parsed.a, local);
		var count = 0;
		local.positions = local.positions || {};
		var uid = $uid(this);
		if (!local.positions[uid]){
			var self = this;
			while ((self = self.previousSibling)){
				if (self.nodeType != 1) continue;
				count ++;
				var position = local.positions[$uid(self)];
				if (position != undefined){
					count = position + count;
					break;
				}
			}
			local.positions[uid] = count;
		}
		return (local.positions[uid] % parsed.a == parsed.b);
	},

	// custom pseudo selectors

	index: function(index){
		var element = this, count = 0;
		while ((element = element.previousSibling)){
			if (element.nodeType == 1 && ++count > index) return false;
		}
		return (count == index);
	},

	even: function(argument, local){
		return Selectors.Pseudo['nth-child'].call(this, '2n+1', local);
	},

	odd: function(argument, local){
		return Selectors.Pseudo['nth-child'].call(this, '2n', local);
	},
	
	selected: function(){
		return this.selected;
	},
	
	enabled: function(){
		return (this.disabled === false);
	}

});

/*
---

script: Event.js

description: Contains the Event Class, to make the event object cross-browser.

license: MIT-style license.

requires:
- /Window
- /Document
- /Hash
- /Array
- /Function
- /String

provides: [Event]

...
*/

var Event = new Native({

	name: 'Event',

	initialize: function(event, win){
		win = win || window;
		var doc = win.document;
		event = event || win.event;
		if (event.$extended) return event;
		this.$extended = true;
		var type = event.type;
		var target = event.target || event.srcElement;
		while (target && target.nodeType == 3) target = target.parentNode;

		if (type.test(/key/)){
			var code = event.which || event.keyCode;
			var key = Event.Keys.keyOf(code);
			if (type == 'keydown'){
				var fKey = code - 111;
				if (fKey > 0 && fKey < 13) key = 'f' + fKey;
			}
			key = key || String.fromCharCode(code).toLowerCase();
		} else if (type.match(/(click|mouse|menu)/i)){
			doc = (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
			var page = {
				x: event.pageX || event.clientX + doc.scrollLeft,
				y: event.pageY || event.clientY + doc.scrollTop
			};
			var client = {
				x: (event.pageX) ? event.pageX - win.pageXOffset : event.clientX,
				y: (event.pageY) ? event.pageY - win.pageYOffset : event.clientY
			};
			if (type.match(/DOMMouseScroll|mousewheel/)){
				var wheel = (event.wheelDelta) ? event.wheelDelta / 120 : -(event.detail || 0) / 3;
			}
			var rightClick = (event.which == 3) || (event.button == 2);
			var related = null;
			if (type.match(/over|out/)){
				switch (type){
					case 'mouseover': related = event.relatedTarget || event.fromElement; break;
					case 'mouseout': related = event.relatedTarget || event.toElement;
				}
				if (!(function(){
					while (related && related.nodeType == 3) related = related.parentNode;
					return true;
				}).create({attempt: Browser.Engine.gecko})()) related = false;
			}
		}

		return $extend(this, {
			event: event,
			type: type,

			page: page,
			client: client,
			rightClick: rightClick,

			wheel: wheel,

			relatedTarget: related,
			target: target,

			code: code,
			key: key,

			shift: event.shiftKey,
			control: event.ctrlKey,
			alt: event.altKey,
			meta: event.metaKey
		});
	}

});

Event.Keys = new Hash({
	'enter': 13,
	'up': 38,
	'down': 40,
	'left': 37,
	'right': 39,
	'esc': 27,
	'space': 32,
	'backspace': 8,
	'tab': 9,
	'delete': 46
});

Event.implement({

	stop: function(){
		return this.stopPropagation().preventDefault();
	},

	stopPropagation: function(){
		if (this.event.stopPropagation) this.event.stopPropagation();
		else this.event.cancelBubble = true;
		return this;
	},

	preventDefault: function(){
		if (this.event.preventDefault) this.event.preventDefault();
		else this.event.returnValue = false;
		return this;
	}

});

/*
---

script: Element.Event.js

description: Contains Element methods for dealing with events. This file also includes mouseenter and mouseleave custom Element Events.

license: MIT-style license.

requires: 
- /Element
- /Event

provides: [Element.Event]

...
*/

Element.Properties.events = {set: function(events){
	this.addEvents(events);
}};

Native.implement([Element, Window, Document], {

	addEvent: function(type, fn){
		var events = this.retrieve('events', {});
		events[type] = events[type] || {'keys': [], 'values': []};
		if (events[type].keys.contains(fn)) return this;
		events[type].keys.push(fn);
		var realType = type, custom = Element.Events.get(type), condition = fn, self = this;
		if (custom){
			if (custom.onAdd) custom.onAdd.call(this, fn);
			if (custom.condition){
				condition = function(event){
					if (custom.condition.call(this, event)) return fn.call(this, event);
					return true;
				};
			}
			realType = custom.base || realType;
		}
		var defn = function(){
			return fn.call(self);
		};
		var nativeEvent = Element.NativeEvents[realType];
		if (nativeEvent){
			if (nativeEvent == 2){
				defn = function(event){
					event = new Event(event, self.getWindow());
					if (condition.call(self, event) === false) event.stop();
				};
			}
			this.addListener(realType, defn);
		}
		events[type].values.push(defn);
		return this;
	},

	removeEvent: function(type, fn){
		var events = this.retrieve('events');
		if (!events || !events[type]) return this;
		var pos = events[type].keys.indexOf(fn);
		if (pos == -1) return this;
		events[type].keys.splice(pos, 1);
		var value = events[type].values.splice(pos, 1)[0];
		var custom = Element.Events.get(type);
		if (custom){
			if (custom.onRemove) custom.onRemove.call(this, fn);
			type = custom.base || type;
		}
		return (Element.NativeEvents[type]) ? this.removeListener(type, value) : this;
	},

	addEvents: function(events){
		for (var event in events) this.addEvent(event, events[event]);
		return this;
	},

	removeEvents: function(events){
		var type;
		if ($type(events) == 'object'){
			for (type in events) this.removeEvent(type, events[type]);
			return this;
		}
		var attached = this.retrieve('events');
		if (!attached) return this;
		if (!events){
			for (type in attached) this.removeEvents(type);
			this.eliminate('events');
		} else if (attached[events]){
			while (attached[events].keys[0]) this.removeEvent(events, attached[events].keys[0]);
			attached[events] = null;
		}
		return this;
	},

	fireEvent: function(type, args, delay){
		var events = this.retrieve('events');
		if (!events || !events[type]) return this;
		events[type].keys.each(function(fn){
			fn.create({'bind': this, 'delay': delay, 'arguments': args})();
		}, this);
		return this;
	},

	cloneEvents: function(from, type){
		from = document.id(from);
		var fevents = from.retrieve('events');
		if (!fevents) return this;
		if (!type){
			for (var evType in fevents) this.cloneEvents(from, evType);
		} else if (fevents[type]){
			fevents[type].keys.each(function(fn){
				this.addEvent(type, fn);
			}, this);
		}
		return this;
	}

});

Element.NativeEvents = {
	click: 2, dblclick: 2, mouseup: 2, mousedown: 2, contextmenu: 2, //mouse buttons
	mousewheel: 2, DOMMouseScroll: 2, //mouse wheel
	mouseover: 2, mouseout: 2, mousemove: 2, selectstart: 2, selectend: 2, //mouse movement
	keydown: 2, keypress: 2, keyup: 2, //keyboard
	focus: 2, blur: 2, change: 2, reset: 2, select: 2, submit: 2, //form elements
	load: 1, unload: 1, beforeunload: 2, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1, //window
	error: 1, abort: 1, scroll: 1 //misc
};

(function(){

var $check = function(event){
	var related = event.relatedTarget;
	if (related == undefined) return true;
	if (related === false) return false;
	return ($type(this) != 'document' && related != this && related.prefix != 'xul' && !this.hasChild(related));
};

Element.Events = new Hash({

	mouseenter: {
		base: 'mouseover',
		condition: $check
	},

	mouseleave: {
		base: 'mouseout',
		condition: $check
	},

	mousewheel: {
		base: (Browser.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel'
	}

});

})();

/*
---

script: Class.js

description: Contains the Class Function for easily creating, extending, and implementing reusable Classes.

license: MIT-style license.

requires:
- /$util
- /Native
- /Array
- /String
- /Function
- /Number
- /Hash

provides: [Class]

...
*/

function Class(params){
	
	if (params instanceof Function) params = {initialize: params};
	
	var newClass = function(){
		Object.reset(this);
		if (newClass._prototyping) return this;
		this._current = $empty;
		var value = (this.initialize) ? this.initialize.apply(this, arguments) : this;
		delete this._current; delete this.caller;
		return value;
	}.extend(this);
	
	newClass.implement(params);
	
	newClass.constructor = Class;
	newClass.prototype.constructor = newClass;

	return newClass;

};

Function.prototype.protect = function(){
	this._protected = true;
	return this;
};

Object.reset = function(object, key){
		
	if (key == null){
		for (var p in object) Object.reset(object, p);
		return object;
	}
	
	delete object[key];
	
	switch ($type(object[key])){
		case 'object':
			var F = function(){};
			F.prototype = object[key];
			var i = new F;
			object[key] = Object.reset(i);
		break;
		case 'array': object[key] = $unlink(object[key]); break;
	}
	
	return object;
	
};

new Native({name: 'Class', initialize: Class}).extend({

	instantiate: function(F){
		F._prototyping = true;
		var proto = new F;
		delete F._prototyping;
		return proto;
	},
	
	wrap: function(self, key, method){
		if (method._origin) method = method._origin;
		
		return function(){
			if (method._protected && this._current == null) throw new Error('The method "' + key + '" cannot be called.');
			var caller = this.caller, current = this._current;
			this.caller = current; this._current = arguments.callee;
			var result = method.apply(this, arguments);
			this._current = current; this.caller = caller;
			return result;
		}.extend({_owner: self, _origin: method, _name: key});

	}
	
});

Class.implement({
	
	implement: function(key, value){
		
		if ($type(key) == 'object'){
			for (var p in key) this.implement(p, key[p]);
			return this;
		}
		
		var mutator = Class.Mutators[key];
		
		if (mutator){
			value = mutator.call(this, value);
			if (value == null) return this;
		}
		
		var proto = this.prototype;

		switch ($type(value)){
			
			case 'function':
				if (value._hidden) return this;
				proto[key] = Class.wrap(this, key, value);
			break;
			
			case 'object':
				var previous = proto[key];
				if ($type(previous) == 'object') $mixin(previous, value);
				else proto[key] = $unlink(value);
			break;
			
			case 'array':
				proto[key] = $unlink(value);
			break;
			
			default: proto[key] = value;

		}
		
		return this;

	}
	
});

Class.Mutators = {
	
	Extends: function(parent){

		this.parent = parent;
		this.prototype = Class.instantiate(parent);

		this.implement('parent', function(){
			var name = this.caller._name, previous = this.caller._owner.parent.prototype[name];
			if (!previous) throw new Error('The method "' + name + '" has no parent.');
			return previous.apply(this, arguments);
		}.protect());

	},

	Implements: function(items){
		$splat(items).each(function(item){
			if (item instanceof Function) item = Class.instantiate(item);
			this.implement(item);
		}, this);

	}
	
};

/*
---

script: Class.Extras.js

description: Contains Utility Classes that can be implemented into your own Classes to ease the execution of many common tasks.

license: MIT-style license.

requires:
- /Class

provides: [Chain, Events, Options]

...
*/

var Chain = new Class({

	$chain: [],

	chain: function(){
		this.$chain.extend(Array.flatten(arguments));
		return this;
	},

	callChain: function(){
		return (this.$chain.length) ? this.$chain.shift().apply(this, arguments) : false;
	},

	clearChain: function(){
		this.$chain.empty();
		return this;
	}

});

var Events = new Class({

	$events: {},

	addEvent: function(type, fn, internal){
		type = Events.removeOn(type);
		if (fn != $empty){
			this.$events[type] = this.$events[type] || [];
			this.$events[type].include(fn);
			if (internal) fn.internal = true;
		}
		return this;
	},

	addEvents: function(events){
		for (var type in events) this.addEvent(type, events[type]);
		return this;
	},

	fireEvent: function(type, args, delay){
		type = Events.removeOn(type);
		if (!this.$events || !this.$events[type]) return this;
		this.$events[type].each(function(fn){
			fn.create({'bind': this, 'delay': delay, 'arguments': args})();
		}, this);
		return this;
	},

	removeEvent: function(type, fn){
		type = Events.removeOn(type);
		if (!this.$events[type]) return this;
		if (!fn.internal) this.$events[type].erase(fn);
		return this;
	},

	removeEvents: function(events){
		var type;
		if ($type(events) == 'object'){
			for (type in events) this.removeEvent(type, events[type]);
			return this;
		}
		if (events) events = Events.removeOn(events);
		for (type in this.$events){
			if (events && events != type) continue;
			var fns = this.$events[type];
			for (var i = fns.length; i--; i) this.removeEvent(type, fns[i]);
		}
		return this;
	}

});

Events.removeOn = function(string){
	return string.replace(/^on([A-Z])/, function(full, first){
		return first.toLowerCase();
	});
};

var Options = new Class({

	setOptions: function(){
		this.options = $merge.run([this.options].extend(arguments));
		if (!this.addEvent) return this;
		for (var option in this.options){
			if ($type(this.options[option]) != 'function' || !(/^on[A-Z]/).test(option)) continue;
			this.addEvent(option, this.options[option]);
			delete this.options[option];
		}
		return this;
	}

});

/*
---

script: Request.js

description: Powerful all purpose Request Class. Uses XMLHTTPRequest.

license: MIT-style license.

requires:
- /Element
- /Chain
- /Events
- /Options
- /Browser

provides: [Request]

...
*/

var Request = new Class({

	Implements: [Chain, Events, Options],

	options: {/*
		onRequest: $empty,
		onComplete: $empty,
		onCancel: $empty,
		onSuccess: $empty,
		onFailure: $empty,
		onException: $empty,*/
		url: '',
		data: '',
		headers: {
			'X-Requested-With': 'XMLHttpRequest',
			'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
		},
		async: true,
		format: false,
		method: 'post',
		link: 'ignore',
		isSuccess: null,
		emulation: true,
		urlEncoded: true,
		encoding: 'utf-8',
		evalScripts: false,
		evalResponse: false,
		noCache: false
	},

	initialize: function(options){
		this.xhr = new Browser.Request();
		this.setOptions(options);
		this.options.isSuccess = this.options.isSuccess || this.isSuccess;
		this.headers = new Hash(this.options.headers);
	},

	onStateChange: function(){
		if (this.xhr.readyState != 4 || !this.running) return;
		this.running = false;
		this.status = 0;
		$try(function(){
			this.status = this.xhr.status;
		}.bind(this));
		this.xhr.onreadystatechange = $empty;
		if (this.options.isSuccess.call(this, this.status)){
			this.response = {text: this.xhr.responseText, xml: this.xhr.responseXML};
			this.success(this.response.text, this.response.xml);
		} else {
			this.response = {text: null, xml: null};
			this.failure();
		}
	},

	isSuccess: function(){
		return ((this.status >= 200) && (this.status < 300));
	},

	processScripts: function(text){
		if (this.options.evalResponse || (/(ecma|java)script/).test(this.getHeader('Content-type'))) return $exec(text);
		return text.stripScripts(this.options.evalScripts);
	},

	success: function(text, xml){
		this.onSuccess(this.processScripts(text), xml);
	},

	onSuccess: function(){
		this.fireEvent('complete', arguments).fireEvent('success', arguments).callChain();
	},

	failure: function(){
		this.onFailure();
	},

	onFailure: function(){
		this.fireEvent('complete').fireEvent('failure', this.xhr);
	},

	setHeader: function(name, value){
		this.headers.set(name, value);
		return this;
	},

	getHeader: function(name){
		return $try(function(){
			return this.xhr.getResponseHeader(name);
		}.bind(this));
	},

	check: function(){
		if (!this.running) return true;
		switch (this.options.link){
			case 'cancel': this.cancel(); return true;
			case 'chain': this.chain(this.caller.bind(this, arguments)); return false;
		}
		return false;
	},

	send: function(options){
		if (!this.check(options)) return this;
		this.running = true;

		var type = $type(options);
		if (type == 'string' || type == 'element') options = {data: options};

		var old = this.options;
		options = $extend({data: old.data, url: old.url, method: old.method}, options);
		var data = options.data, url = String(options.url), method = options.method.toLowerCase();

		switch ($type(data)){
			case 'element': data = document.id(data).toQueryString(); break;
			case 'object': case 'hash': data = Hash.toQueryString(data);
		}

		if (this.options.format){
			var format = 'format=' + this.options.format;
			data = (data) ? format + '&' + data : format;
		}

		if (this.options.emulation && !['get', 'post'].contains(method)){
			var _method = '_method=' + method;
			data = (data) ? _method + '&' + data : _method;
			method = 'post';
		}

		if (this.options.urlEncoded && method == 'post'){
			var encoding = (this.options.encoding) ? '; charset=' + this.options.encoding : '';
			this.headers.set('Content-type', 'application/x-www-form-urlencoded' + encoding);
		}

		if (this.options.noCache){
			var noCache = 'noCache=' + new Date().getTime();
			data = (data) ? noCache + '&' + data : noCache;
		}

		var trimPosition = url.lastIndexOf('/');
		if (trimPosition > -1 && (trimPosition = url.indexOf('#')) > -1) url = url.substr(0, trimPosition);

		if (data && method == 'get'){
			url = url + (url.contains('?') ? '&' : '?') + data;
			data = null;
		}

		this.xhr.open(method.toUpperCase(), url, this.options.async);

		this.xhr.onreadystatechange = this.onStateChange.bind(this);

		this.headers.each(function(value, key){
			try {
				this.xhr.setRequestHeader(key, value);
			} catch (e){
				this.fireEvent('exception', [key, value]);
			}
		}, this);

		this.fireEvent('request');
		this.xhr.send(data);
		if (!this.options.async) this.onStateChange();
		return this;
	},

	cancel: function(){
		if (!this.running) return this;
		this.running = false;
		this.xhr.abort();
		this.xhr.onreadystatechange = $empty;
		this.xhr = new Browser.Request();
		this.fireEvent('cancel');
		return this;
	}

});

(function(){

var methods = {};
['get', 'post', 'put', 'delete', 'GET', 'POST', 'PUT', 'DELETE'].each(function(method){
	methods[method] = function(){
		var params = Array.link(arguments, {url: String.type, data: $defined});
		return this.send($extend(params, {method: method}));
	};
});

Request.implement(methods);

})();

Element.Properties.send = {

	set: function(options){
		var send = this.retrieve('send');
		if (send) send.cancel();
		return this.eliminate('send').store('send:options', $extend({
			data: this, link: 'cancel', method: this.get('method') || 'post', url: this.get('action')
		}, options));
	},

	get: function(options){
		if (options || !this.retrieve('send')){
			if (options || !this.retrieve('send:options')) this.set('send', options);
			this.store('send', new Request(this.retrieve('send:options')));
		}
		return this.retrieve('send');
	}

};

Element.implement({

	send: function(url){
		var sender = this.get('send');
		sender.send({data: this, url: url || sender.options.url});
		return this;
	}

});

/*
---

script: Request.HTML.js

description: Extends the basic Request Class with additional methods for interacting with HTML responses.

license: MIT-style license.

requires:
- /Request
- /Element

provides: [Request.HTML]

...
*/

Request.HTML = new Class({

	Extends: Request,

	options: {
		update: false,
		append: false,
		evalScripts: true,
		filter: false
	},

	processHTML: function(text){
		var match = text.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
		text = (match) ? match[1] : text;

		var container = new Element('div');

		return $try(function(){
			var root = '<root>' + text + '</root>', doc;
			if (Browser.Engine.trident){
				doc = new ActiveXObject('Microsoft.XMLDOM');
				doc.async = false;
				doc.loadXML(root);
			} else {
				doc = new DOMParser().parseFromString(root, 'text/xml');
			}
			root = doc.getElementsByTagName('root')[0];
			if (!root) return null;
			for (var i = 0, k = root.childNodes.length; i < k; i++){
				var child = Element.clone(root.childNodes[i], true, true);
				if (child) container.grab(child);
			}
			return container;
		}) || container.set('html', text);
	},

	success: function(text){
		var options = this.options, response = this.response;

		response.html = text.stripScripts(function(script){
			response.javascript = script;
		});

		var temp = this.processHTML(response.html);

		response.tree = temp.childNodes;
		response.elements = temp.getElements('*');

		if (options.filter) response.tree = response.elements.filter(options.filter);
		if (options.update) document.id(options.update).empty().set('html', response.html);
		else if (options.append) document.id(options.append).adopt(temp.getChildren());
		if (options.evalScripts) $exec(response.javascript);

		this.onSuccess(response.tree, response.elements, response.html, response.javascript);
	}

});

Element.Properties.load = {

	set: function(options){
		var load = this.retrieve('load');
		if (load) load.cancel();
		return this.eliminate('load').store('load:options', $extend({data: this, link: 'cancel', update: this, method: 'get'}, options));
	},

	get: function(options){
		if (options || ! this.retrieve('load')){
			if (options || !this.retrieve('load:options')) this.set('load', options);
			this.store('load', new Request.HTML(this.retrieve('load:options')));
		}
		return this.retrieve('load');
	}

};

Element.implement({

	load: function(){
		this.get('load').send(Array.link(arguments, {data: Object.type, url: String.type}));
		return this;
	}

});

/*
---

script: Fx.js

description: Contains the basic animation logic to be extended by all other Fx Classes.

license: MIT-style license.

requires:
- /Chain
- /Events
- /Options

provides: [Fx]

...
*/

var Fx = new Class({

	Implements: [Chain, Events, Options],

	options: {
		/*
		onStart: $empty,
		onCancel: $empty,
		onComplete: $empty,
		*/
		fps: 50,
		unit: false,
		duration: 500,
		link: 'ignore'
	},

	initialize: function(options){
		this.subject = this.subject || this;
		this.setOptions(options);
		this.options.duration = Fx.Durations[this.options.duration] || this.options.duration.toInt();
		var wait = this.options.wait;
		if (wait === false) this.options.link = 'cancel';
	},

	getTransition: function(){
		return function(p){
			return -(Math.cos(Math.PI * p) - 1) / 2;
		};
	},

	step: function(){
		var time = $time();
		if (time < this.time + this.options.duration){
			var delta = this.transition((time - this.time) / this.options.duration);
			this.set(this.compute(this.from, this.to, delta));
		} else {
			this.set(this.compute(this.from, this.to, 1));
			this.complete();
		}
	},

	set: function(now){
		return now;
	},

	compute: function(from, to, delta){
		return Fx.compute(from, to, delta);
	},

	check: function(){
		if (!this.timer) return true;
		switch (this.options.link){
			case 'cancel': this.cancel(); return true;
			case 'chain': this.chain(this.caller.bind(this, arguments)); return false;
		}
		return false;
	},

	start: function(from, to){
		if (!this.check(from, to)) return this;
		this.from = from;
		this.to = to;
		this.time = 0;
		this.transition = this.getTransition();
		this.startTimer();
		this.onStart();
		return this;
	},

	complete: function(){
		if (this.stopTimer()) this.onComplete();
		return this;
	},

	cancel: function(){
		if (this.stopTimer()) this.onCancel();
		return this;
	},

	onStart: function(){
		this.fireEvent('start', this.subject);
	},

	onComplete: function(){
		this.fireEvent('complete', this.subject);
		if (!this.callChain()) this.fireEvent('chainComplete', this.subject);
	},

	onCancel: function(){
		this.fireEvent('cancel', this.subject).clearChain();
	},

	pause: function(){
		this.stopTimer();
		return this;
	},

	resume: function(){
		this.startTimer();
		return this;
	},

	stopTimer: function(){
		if (!this.timer) return false;
		this.time = $time() - this.time;
		this.timer = $clear(this.timer);
		return true;
	},

	startTimer: function(){
		if (this.timer) return false;
		this.time = $time() - this.time;
		this.timer = this.step.periodical(Math.round(1000 / this.options.fps), this);
		return true;
	}

});

Fx.compute = function(from, to, delta){
	return (to - from) * delta + from;
};

Fx.Durations = {'short': 250, 'normal': 500, 'long': 1000};

/*
---

script: Fx.CSS.js

description: Contains the CSS animation logic. Used by Fx.Tween, Fx.Morph, Fx.Elements.

license: MIT-style license.

requires:
- /Fx
- /Element.Style

provides: [Fx.CSS]

...
*/

Fx.CSS = new Class({

	Extends: Fx,

	//prepares the base from/to object

	prepare: function(element, property, values){
		values = $splat(values);
		var values1 = values[1];
		if (!$chk(values1)){
			values[1] = values[0];
			values[0] = element.getStyle(property);
		}
		var parsed = values.map(this.parse);
		return {from: parsed[0], to: parsed[1]};
	},

	//parses a value into an array

	parse: function(value){
		value = $lambda(value)();
		value = (typeof value == 'string') ? value.split(' ') : $splat(value);
		return value.map(function(val){
			val = String(val);
			var found = false;
			Fx.CSS.Parsers.each(function(parser, key){
				if (found) return;
				var parsed = parser.parse(val);
				if ($chk(parsed)) found = {value: parsed, parser: parser};
			});
			found = found || {value: val, parser: Fx.CSS.Parsers.String};
			return found;
		});
	},

	//computes by a from and to prepared objects, using their parsers.

	compute: function(from, to, delta){
		var computed = [];
		(Math.min(from.length, to.length)).times(function(i){
			computed.push({value: from[i].parser.compute(from[i].value, to[i].value, delta), parser: from[i].parser});
		});
		computed.$family = {name: 'fx:css:value'};
		return computed;
	},

	//serves the value as settable

	serve: function(value, unit){
		if ($type(value) != 'fx:css:value') value = this.parse(value);
		var returned = [];
		value.each(function(bit){
			returned = returned.concat(bit.parser.serve(bit.value, unit));
		});
		return returned;
	},

	//renders the change to an element

	render: function(element, property, value, unit){
		element.setStyle(property, this.serve(value, unit));
	},

	//searches inside the page css to find the values for a selector

	search: function(selector){
		if (Fx.CSS.Cache[selector]) return Fx.CSS.Cache[selector];
		var to = {};
		Array.each(document.styleSheets, function(sheet, j){
			var href = sheet.href;
			if (href && href.contains('://') && !href.contains(document.domain)) return;
			var rules = sheet.rules || sheet.cssRules;
			Array.each(rules, function(rule, i){
				if (!rule.style) return;
				var selectorText = (rule.selectorText) ? rule.selectorText.replace(/^\w+/, function(m){
					return m.toLowerCase();
				}) : null;
				if (!selectorText || !selectorText.test('^' + selector + '$')) return;
				Element.Styles.each(function(value, style){
					if (!rule.style[style] || Element.ShortStyles[style]) return;
					value = String(rule.style[style]);
					to[style] = (value.test(/^rgb/)) ? value.rgbToHex() : value;
				});
			});
		});
		return Fx.CSS.Cache[selector] = to;
	}

});

Fx.CSS.Cache = {};

Fx.CSS.Parsers = new Hash({

	Color: {
		parse: function(value){
			if (value.match(/^#[0-9a-f]{3,6}$/i)) return value.hexToRgb(true);
			return ((value = value.match(/(\d+),\s*(\d+),\s*(\d+)/))) ? [value[1], value[2], value[3]] : false;
		},
		compute: function(from, to, delta){
			return from.map(function(value, i){
				return Math.round(Fx.compute(from[i], to[i], delta));
			});
		},
		serve: function(value){
			return value.map(Number);
		}
	},

	Number: {
		parse: parseFloat,
		compute: Fx.compute,
		serve: function(value, unit){
			return (unit) ? value + unit : value;
		}
	},

	String: {
		parse: $lambda(false),
		compute: $arguments(1),
		serve: $arguments(0)
	}

});

/*
---

script: Fx.Tween.js

description: Formerly Fx.Style, effect to transition any CSS property for an element.

license: MIT-style license.

requires: 
- /Fx.CSS

provides: [Fx.Tween, Element.fade, Element.highlight]

...
*/

Fx.Tween = new Class({

	Extends: Fx.CSS,

	initialize: function(element, options){
		this.element = this.subject = document.id(element);
		this.parent(options);
	},

	set: function(property, now){
		if (arguments.length == 1){
			now = property;
			property = this.property || this.options.property;
		}
		this.render(this.element, property, now, this.options.unit);
		return this;
	},

	start: function(property, from, to){
		if (!this.check(property, from, to)) return this;
		var args = Array.flatten(arguments);
		this.property = this.options.property || args.shift();
		var parsed = this.prepare(this.element, this.property, args);
		return this.parent(parsed.from, parsed.to);
	}

});

Element.Properties.tween = {

	set: function(options){
		var tween = this.retrieve('tween');
		if (tween) tween.cancel();
		return this.eliminate('tween').store('tween:options', $extend({link: 'cancel'}, options));
	},

	get: function(options){
		if (options || !this.retrieve('tween')){
			if (options || !this.retrieve('tween:options')) this.set('tween', options);
			this.store('tween', new Fx.Tween(this, this.retrieve('tween:options')));
		}
		return this.retrieve('tween');
	}

};

Element.implement({

	tween: function(property, from, to){
		this.get('tween').start(arguments);
		return this;
	},

	fade: function(how){
		var fade = this.get('tween'), o = 'opacity', toggle;
		how = $pick(how, 'toggle');
		switch (how){
			case 'in': fade.start(o, 1); break;
			case 'out': fade.start(o, 0); break;
			case 'show': fade.set(o, 1); break;
			case 'hide': fade.set(o, 0); break;
			case 'toggle':
				var flag = this.retrieve('fade:flag', this.get('opacity') == 1);
				fade.start(o, (flag) ? 0 : 1);
				this.store('fade:flag', !flag);
				toggle = true;
			break;
			default: fade.start(o, arguments);
		}
		if (!toggle) this.eliminate('fade:flag');
		return this;
	},

	highlight: function(start, end){
		if (!end){
			end = this.retrieve('highlight:original', this.getStyle('background-color'));
			end = (end == 'transparent') ? '#fff' : end;
		}
		var tween = this.get('tween');
		tween.start('background-color', start || '#ffff88', end).chain(function(){
			this.setStyle('background-color', this.retrieve('highlight:original'));
			tween.callChain();
		}.bind(this));
		return this;
	}

});

/*
---

script: Fx.Transitions.js

description: Contains a set of advanced transitions to be used with any of the Fx Classes.

license: MIT-style license.

credits:
- Easing Equations by Robert Penner, <http://www.robertpenner.com/easing/>, modified and optimized to be used with MooTools.

requires:
- /Fx

provides: [Fx.Transitions]

...
*/

Fx.implement({

	getTransition: function(){
		var trans = this.options.transition || Fx.Transitions.Sine.easeInOut;
		if (typeof trans == 'string'){
			var data = trans.split(':');
			trans = Fx.Transitions;
			trans = trans[data[0]] || trans[data[0].capitalize()];
			if (data[1]) trans = trans['ease' + data[1].capitalize() + (data[2] ? data[2].capitalize() : '')];
		}
		return trans;
	}

});

Fx.Transition = function(transition, params){
	params = $splat(params);
	return $extend(transition, {
		easeIn: function(pos){
			return transition(pos, params);
		},
		easeOut: function(pos){
			return 1 - transition(1 - pos, params);
		},
		easeInOut: function(pos){
			return (pos <= 0.5) ? transition(2 * pos, params) / 2 : (2 - transition(2 * (1 - pos), params)) / 2;
		}
	});
};

Fx.Transitions = new Hash({

	linear: $arguments(0)

});

Fx.Transitions.extend = function(transitions){
	for (var transition in transitions) Fx.Transitions[transition] = new Fx.Transition(transitions[transition]);
};

Fx.Transitions.extend({

	Pow: function(p, x){
		return Math.pow(p, x[0] || 6);
	},

	Expo: function(p){
		return Math.pow(2, 8 * (p - 1));
	},

	Circ: function(p){
		return 1 - Math.sin(Math.acos(p));
	},

	Sine: function(p){
		return 1 - Math.sin((1 - p) * Math.PI / 2);
	},

	Back: function(p, x){
		x = x[0] || 1.618;
		return Math.pow(p, 2) * ((x + 1) * p - x);
	},

	Bounce: function(p){
		var value;
		for (var a = 0, b = 1; 1; a += b, b /= 2){
			if (p >= (7 - 4 * a) / 11){
				value = b * b - Math.pow((11 - 6 * a - 11 * p) / 4, 2);
				break;
			}
		}
		return value;
	},

	Elastic: function(p, x){
		return Math.pow(2, 10 * --p) * Math.cos(20 * p * Math.PI * (x[0] || 1) / 3);
	}

});

['Quad', 'Cubic', 'Quart', 'Quint'].each(function(transition, i){
	Fx.Transitions[transition] = new Fx.Transition(function(p){
		return Math.pow(p, [i + 2]);
	});
});

/*
---

script: Fx.Morph.js

description: Formerly Fx.Styles, effect to transition any number of CSS properties for an element using an object of rules, or CSS based selector rules.

license: MIT-style license.

requires:
- /Fx.CSS

provides: [Fx.Morph]

...
*/

Fx.Morph = new Class({

	Extends: Fx.CSS,

	initialize: function(element, options){
		this.element = this.subject = document.id(element);
		this.parent(options);
	},

	set: function(now){
		if (typeof now == 'string') now = this.search(now);
		for (var p in now) this.render(this.element, p, now[p], this.options.unit);
		return this;
	},

	compute: function(from, to, delta){
		var now = {};
		for (var p in from) now[p] = this.parent(from[p], to[p], delta);
		return now;
	},

	start: function(properties){
		if (!this.check(properties)) return this;
		if (typeof properties == 'string') properties = this.search(properties);
		var from = {}, to = {};
		for (var p in properties){
			var parsed = this.prepare(this.element, p, properties[p]);
			from[p] = parsed.from;
			to[p] = parsed.to;
		}
		return this.parent(from, to);
	}

});

Element.Properties.morph = {

	set: function(options){
		var morph = this.retrieve('morph');
		if (morph) morph.cancel();
		return this.eliminate('morph').store('morph:options', $extend({link: 'cancel'}, options));
	},

	get: function(options){
		if (options || !this.retrieve('morph')){
			if (options || !this.retrieve('morph:options')) this.set('morph', options);
			this.store('morph', new Fx.Morph(this, this.retrieve('morph:options')));
		}
		return this.retrieve('morph');
	}

};

Element.implement({

	morph: function(props){
		this.get('morph').start(props);
		return this;
	}

});

/*
---

script: DomReady.js

description: Contains the custom event domready.

license: MIT-style license.

requires:
- /Element.Event

provides: [DomReady]

...
*/

Element.Events.domready = {

	onAdd: function(fn){
		if (Browser.loaded) fn.call(this);
	}

};

(function(){

	var domready = function(){
		if (Browser.loaded) return;
		Browser.loaded = true;
		window.fireEvent('domready');
		document.fireEvent('domready');
	};
	
	window.addEvent('load', domready);

	if (Browser.Engine.trident){
		var temp = document.createElement('div');
		(function(){
			($try(function(){
				temp.doScroll(); // Technique by Diego Perini
				return document.id(temp).inject(document.body).set('html', 'temp').dispose();
			})) ? domready() : arguments.callee.delay(50);
		})();
	} else if (Browser.Engine.webkit && Browser.Engine.version < 525){
		(function(){
			(['loaded', 'complete'].contains(document.readyState)) ? domready() : arguments.callee.delay(50);
		})();
	} else {
		document.addEvent('DOMContentLoaded', domready);
	}

})();

/*
---

script: Cookie.js

description: Class for creating, reading, and deleting browser Cookies.

license: MIT-style license.

credits:
- Based on the functions by Peter-Paul Koch (http://quirksmode.org).

requires:
- /Options

provides: [Cookie]

...
*/

var Cookie = new Class({

	Implements: Options,

	options: {
		path: false,
		domain: false,
		duration: false,
		secure: false,
		document: document
	},

	initialize: function(key, options){
		this.key = key;
		this.setOptions(options);
	},

	write: function(value){
		value = encodeURIComponent(value);
		if (this.options.domain) value += '; domain=' + this.options.domain;
		if (this.options.path) value += '; path=' + this.options.path;
		if (this.options.duration){
			var date = new Date();
			date.setTime(date.getTime() + this.options.duration * 24 * 60 * 60 * 1000);
			value += '; expires=' + date.toGMTString();
		}
		if (this.options.secure) value += '; secure';
		this.options.document.cookie = this.key + '=' + value;
		return this;
	},

	read: function(){
		var value = this.options.document.cookie.match('(?:^|;)\\s*' + this.key.escapeRegExp() + '=([^;]*)');
		return (value) ? decodeURIComponent(value[1]) : null;
	},

	dispose: function(){
		new Cookie(this.key, $merge(this.options, {duration: -1})).write('');
		return this;
	}

});

Cookie.write = function(key, value, options){
	return new Cookie(key, options).write(value);
};

Cookie.read = function(key){
	return new Cookie(key).read();
};

Cookie.dispose = function(key, options){
	return new Cookie(key, options).dispose();
};

/*
---

script: Swiff.js

description: Wrapper for embedding SWF movies. Supports External Interface Communication.

license: MIT-style license.

credits: 
- Flash detection & Internet Explorer + Flash Player 9 fix inspired by SWFObject.

requires:
- /Options
- /$util

provides: [Swiff]

...
*/

var Swiff = new Class({

	Implements: [Options],

	options: {
		id: null,
		height: 1,
		width: 1,
		container: null,
		properties: {},
		params: {
			quality: 'high',
			allowScriptAccess: 'always',
			wMode: 'transparent',
			swLiveConnect: true
		},
		callBacks: {},
		vars: {}
	},

	toElement: function(){
		return this.object;
	},

	initialize: function(path, options){
		this.instance = 'Swiff_' + $time();

		this.setOptions(options);
		options = this.options;
		var id = this.id = options.id || this.instance;
		var container = document.id(options.container);

		Swiff.CallBacks[this.instance] = {};

		var params = options.params, vars = options.vars, callBacks = options.callBacks;
		var properties = $extend({height: options.height, width: options.width}, options.properties);

		var self = this;

		for (var callBack in callBacks){
			Swiff.CallBacks[this.instance][callBack] = (function(option){
				return function(){
					return option.apply(self.object, arguments);
				};
			})(callBacks[callBack]);
			vars[callBack] = 'Swiff.CallBacks.' + this.instance + '.' + callBack;
		}

		params.flashVars = Hash.toQueryString(vars);
		if (Browser.Engine.trident){
			properties.classid = 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000';
			params.movie = path;
		} else {
			properties.type = 'application/x-shockwave-flash';
			properties.data = path;
		}
		var build = '<object id="' + id + '"';
		for (var property in properties) build += ' ' + property + '="' + properties[property] + '"';
		build += '>';
		for (var param in params){
			if (params[param]) build += '<param name="' + param + '" value="' + params[param] + '" />';
		}
		build += '</object>';
		this.object = ((container) ? container.empty() : new Element('div')).set('html', build).firstChild;
	},

	replaces: function(element){
		element = document.id(element, true);
		element.parentNode.replaceChild(this.toElement(), element);
		return this;
	},

	inject: function(element){
		document.id(element, true).appendChild(this.toElement());
		return this;
	},

	remote: function(){
		return Swiff.remote.apply(Swiff, [this.toElement()].extend(arguments));
	}

});

Swiff.CallBacks = {};

Swiff.remote = function(obj, fn){
	var rs = obj.CallFunction('<invoke name="' + fn + '" returntype="javascript">' + __flash__argumentsToXML(arguments, 2) + '</invoke>');
	return eval(rs);
};

// end Greased MooTools
// hack to circumvent 'bug' when overriding toString (and others):
// https://mootools.lighthouseapp.com/projects/2706/tickets/651-classtostring-broken-on-122-big-regression
['toString', 'toLocaleString', 'valueOf', 'toSource', 'watch', 'unwatch', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable'].each(function (method) {
	Class.Mutators[method] = $arguments(0);
});

//GM_addStyle('.InlineEditorOpen { background-color: #000000 ! important } ');

var getJSVariable = function (regex) {
        // Thanks to Vispillo for this compact code
        var retval;
        $$('script').each( function (script) {
            if (retval != undefined) {
                return;
            }
            var html = script.innerHTML;
            try {
                retval = html.match(regex)[1];
            } catch (e) {
            }
        });
        return retval;
    }

var GM_getGroupId = function () {
    return getJSVariable(/\"search_scope\"[ :]+\"([^\"]+)\"/);
}
var GM_getSessionKey = function () {
    return getJSVariable(/\"api_key\"[ :]+\"([^\"]+)\"/);
}

if (Browser.Engine.webkit || // Chrome, Safari
    Browser.Engine.presto) { // Opera

    var keyPrefix = 'UNIFIEDChallengesCheckPlayAdminTool.';

    GM_getLoggedInUser = function () {
        try {
            return $('TopBar').getElement('table.Header').getElement('td.Status').getElement('a.Pale').get('text'); // use 'text', not 'html' (replaces &amp; with & ..)
        } catch (e) {
            try {
                return $('TopBar').getElement('table.Header').getElement('td.Status').getElement('a.ywa-track').get('text');
            } catch (e) {
                GM_log("unable to retrieve user (" + e + ")");
            }
        }
    }

    GM_getGlobalNsid = function () {
        var reMatch = /global_nsid[ =]+\'([^\']+)\'/;
        var retval;
        $$('script[type=text/javascript]').each( function (script) {
            if ($chk(retval)) {
                return;
            }
            var html = script.get('html');
            if (html.match(reMatch)) {
                try {
                    retval = html.match(reMatch)[1];
                } catch (e) {
                    GM_log("error executing RegExp: " + e);
                    retval = undefined;
                }
            }
        });
        return retval;
    }

    GM_log = function (message) {
        if (Browser.Engine.webkit) {
            console.info(message);
        } else {
            opera.postError(message);
        }
    }

    GM_getValue = function(key, defValue) {
        var retval = window.localStorage.getItem(keyPrefix + key);
        if (retval == null) {
            return defValue;
        }
        return retval;
    }

    GM_setValue = function(key, value) {
        try {
            window.localStorage.setItem(keyPrefix + key, value);
        } catch (e) {
            GM_log("error setting value: " + e);
        }
    }

    GM_deleteValue = function(key) {
        try {
            window.localStorage.removeItem(keyPrefix + key);
        } catch (e) {
            GM_log("error removing value: " + e);
        }
    }

    GM_listValues = function() {
        var list = [];
        var reKey = new RegExp("^" + keyPrefix);
        for (var i = 0, il = window.localStorage.length; i < il; i++) {
            // only use the script's own keys
            var key = window.localStorage.key(i);
            if (key.match(reKey)) {
                list.push(key.replace(keyPrefix, ''));
            }
        }
        return list;
    }

    GM_getObject = function (key) {
        var value = GM_getValue(key);
        if ($chk(value)) {
            return JSON.parse(value);
        }
        return null;
    }

    GM_storeObject = function (key, value) {
        GM_setValue(key, JSON.stringify(value));
    }

    GM_getMagisterLudi = function () {
        // the following api_key is reserved for this application
        // if you need an api_key for your own application, please request one at 
        // http://www.flickr.com/services/apps/create/apply/
        // if you request a Non-Commercial key, you'll get it instantly
        return 'b96f5648216fc3599b3b5fc96e218504'; // the app's own key
    }

    GM_getAuthHash = function () {
        var reMatch = /global_auth_hash[ =]+\'([^\']+)\'/;
        var retval;
        $$('script[type=text/javascript]').each( function (script) {
            if ($chk(retval)) {
                return;
            }
            var html = script.get('html');
            if (html.match(reMatch)) {
                try {
                    retval = html.match(reMatch)[1];
                } catch (e) {
                    GM_log("error executing RegExp: " + e);
                    retval = undefined;
                }
            }
        });
        return retval;
    }

    GM_getAuthToken = function () {
        var reMatch = /global_auth_token[ =]+\'([^\']+)\'/;
        var retval;
        $$('script[type=text/javascript]').each( function (script) {
            if ($chk(retval)) {
                return;
            }
            var html = script.get('html');
            if (html.match(reMatch)) {
                try {
                    retval = html.match(reMatch)[1];
                } catch (e) {
                    GM_log("error executing RegExp: " + e);
                    retval = undefined;
                }
            }
        });
        return retval;
    }

} else {
    GM_getLoggedInUser = function () {
        return unsafeWindow.global_name;
    }

    GM_getGlobalNsid = function () {
        return unsafeWindow.global_nsid;
    }

    GM_getObject = function (key) {
        var value = GM_getValue(key);
        if ($chk(value)) {
            try {
                return JSON.parse(value);
            } catch(e) {
                return eval('(' + value + ')');
            }
        }
        return null;
    }

    GM_storeObject = function (key, value) {
        try {
            GM_setValue(key, JSON.stringify(value));
        } catch (e) {
            GM_setValue(key, uneval(value));
        }
    }

    GM_getMagisterLudi = function () {
        // the following api_key is reserved for this application
        // if you need an api_key for your own application, please request one at 
        // http://www.flickr.com/services/apps/create/apply/
        // if you request a Non-Commercial key, you'll get it instantly
        return 'b96f5648216fc3599b3b5fc96e218504'; // the app's own key
    }

    GM_getAuthHash = function () {
        return unsafeWindow.global_auth_hash;
    }

    GM_getAuthToken = function () {
        return unsafeWindow.global_auth_token;
    }
}

function showUpdateNotification(data) {
    if ($chk($('UCP-update-notification'))) {
        return;
    }
    var checkBeta = data.checkBeta;
    var color = 'white';
    var bgColor = 'black';
    var updatespan = new Element('span', {
	id: 'UCP-update-notification',
        // copied from Google++ userscript:
        styles: {
            padding: '2px 4px',
            background: bgColor + ' none repeat scroll 0% 0%',
            display: 'block',
            '-moz-background-clip': 'border',
            '-moz-background-origin': 'padding',
            '-moz-background-inline-policy': 'continuous',
            position: 'fixed',
            opacity: '0.7',
            'z-index': 100,
            bottom: '5px',
            right: '5px'
        }
    }).inject($(document).getElement("body"));
    new Element('a', {
        html: 'UNIFIED CheckPlay Admin Tool: update available',
        href: 'http://userscripts.org/scripts/show/' + scriptNumber, // Chrome users: first uninstall the old version
        target: '_blank',
        title:  'to the script\'s install page (opens in new tab)',
        styles: {
            'color': color,
            'text-decoration': 'none'
        }
    }).inject(updatespan);
    if (checkBeta == true) {
    	new Element('a', {
        	html: ' (maybe you need this beta version)',
        	href: 'http://userscripts.org/scripts/show/' + betaScriptNumber, // Chrome users: first uninstall the old version
        	target: '_blank',
        	title:  'to the script\'s beta version install page (opens in new tab)',
        	styles: {
            		'color': color,
            		'text-decoration': 'none'
        	}
	}).inject(updatespan);
    }
    new Element('a', {
        html: ' (Changes)',
        title: 'opens in new tab',
        href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623838711730/',
        styles: {
            'text-decoration': 'none'
        },
        target: '_blank'
    }).inject(updatespan);
}

function storeVersion() {
    // only called on iframe containing script's meta data
    var onlineVersion       = $$('body')[0].getElement('pre').get('html').split(/@version\s*/)[1].split(/[\r\n]+/)[0];
    GM_setValue('onlineVersion', onlineVersion); // works only in FF 
}

function outOfDate(onlineVersion) {
        var reVersionMatch      = /(\d+)\.(\d+)\.(\d+)/;
        var onlineVersionParts  = reVersionMatch.exec(onlineVersion);
        var currentVersionParts = reVersionMatch.exec(CPtoolversion);
        var onlineVersionMajor, onlineVersionMinor, onlineVersionBuild;
        //[ onlineVersion, onlineVersionMajor, onlineVersionMinor, onlineVersionBuild ] = onlineVersionParts; 'invalid left-hand side' in Chrome
        onlineVersionMajor = onlineVersionParts[1];
        onlineVersionMinor = onlineVersionParts[2];
        onlineVersionBuild = onlineVersionParts[3];
        var currentVersionMajor, currentVersionMinor, currentVersionBuild;
        //[ currentVersion, currentVersionMajor, currentVersionMinor, currentVersionBuild] = currentVersionParts;
        currentVersionMajor = currentVersionParts[1];
        currentVersionMinor = currentVersionParts[2];
        currentVersionBuild = currentVersionParts[3];
        // first check major: important update! => rewrite, flickr updates, greasemonkey updates
        if (parseInt(onlineVersionMajor, 10) > parseInt(currentVersionMajor, 10)) {
            return true;
        } else if (parseInt(onlineVersionMajor, 10) === parseInt(currentVersionMajor, 10)) { // we don't want to downgrade
            // minor version update => new functionality
            if (parseInt(onlineVersionMinor, 10) > parseInt(currentVersionMinor, 10)) {
                return true;
            } else if (parseInt(onlineVersionMinor, 10) === parseInt(currentVersionMinor, 10)) { // we don't want to downgrade
                // build version update => bugfixes
                if (parseInt(onlineVersionBuild, 10) > parseInt(currentVersionBuild, 10)) {
                    return true;
                }
            }
        }
        return false;
}

function checkVersion() {
    var lastVersionCheckTime = GM_getValue("lastVersionCheckTime");
    var elapsedtime;
    var CPStartTime = new Date();
    if ($chk(lastVersionCheckTime)) {
        elapsedtime = CPStartTime.getTime() - lastVersionCheckTime;
    }
    if (!$chk(lastVersionCheckTime) || elapsedtime / 1000 > 60 * 60 * 12) { //more then 12h ago
        new Element('iframe', {
            src: "http://userscripts.org/scripts/source/" + scriptNumber + ".meta.js", 
            styles: {
                width: 0,
                height: 0,
                display: 'none',
                visibility: 'hidden'
            }
        }).inject($$('body')[0]);
        // the script also runs within this iframe => storeVersion is called
        GM_setValue("lastVersionCheckTime", CPStartTime.getTime().toString());
    }

    var onlineVersion = GM_getValue("onlineVersion");
    if ($chk(onlineVersion)) {
        var updateAvailable = outOfDate(onlineVersion);
        if (updateAvailable) {
            showUpdateNotification({ checkBeta: false });
        }
    }
}



var CPStartTime = new Date();

var UCPprefix = 'UCPA';
// old common library:
var UCPGroupConfigReader = new Class({
    timeBetweenReads: 12 * 60 * 60 * 1000, // 12 hours
    groupListingURL: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623452533109/',
    initialize: function () {
    },
    checkForUpdates: function (groupname, force, callback) {
	if (!$chk(GM_getValue(UCPprefix + ".groupConfig.list"))) {
        	this.readGrouplistURL(false, function (result) {
			if (result.stat == 'error') {
                        	if ($chk(callback)) {
					callback( { stat: 'error', 'error': 'unable to read group list: ' + result.error} );
				} else {
					alert("unable to read group list: " + result.error);
				}
				return;
			}
		});
	}
        if ($chk(GM_getValue(UCPprefix + ".groupConfig." + groupname))) {
            var lastReadTime = GM_getValue(UCPprefix + ".groupConfig.lastReadTime." + groupname);
            var now = new Date().getTime();
            var elapsedTime = $chk(lastReadTime) ? now - lastReadTime : this.timeBetweenReads + 1;
            if (elapsedTime > this.timeBetweenReads || force) {
                GM_log("updating " + groupname + " definitions");
                this.readGroupConfigURL(groupname, force === true ? true : false, callback);
            }
        } else {
            this.readGroupConfigURL(groupname, true, callback);
        }
    },
    createGroupConfig: function (groupname) {
        if (!$chk(groupname)) {
            var reGroupnameMatch = /.*flickr.com\/groups\/([^\/]*)\//;
            groupname = reGroupnameMatch.exec(document.location.href)[1];
        }
        //GM_log("reading config for '" + groupname + "'");
        var storedConfig;
        if ($chk(GM_getValue(UCPprefix + ".groupConfig." + groupname))) {
            storedConfig = GM_getValue(UCPprefix + ".groupConfig." + groupname);
        }
        if ($chk(storedConfig) && storedConfig.match('groupId')) { // test on groupId for versions prior to CL v0.0.7
            try {
                return new UCPGroupConfig(GM_getObject(UCPprefix + ".groupConfig." + groupname));
            } catch (e) {
                // parse error?
                GM_deleteValue(UCPprefix + ".groupConfig." + groupname);
                return null;
            }
        } else {
            // if not available, read from URL, synchronously
            //GM_log("reading configURL for '" + groupname + "' synchronously");
            // make sure the group list is read also: a new group should not have to hit F5 twice, just to get support!
            GM_deleteValue(UCPprefix + ".groupConfig.list");
            this.readGroupConfigURL(groupname, true);
            storedConfig = GM_getValue(UCPprefix + ".groupConfig." + groupname);
            if ($chk(storedConfig)) {
                try {
                    return new UCPGroupConfig(GM_getObject(UCPprefix + ".groupConfig." + groupname));
                } catch (e) {
                    // parse error?
                    GM_deleteValue(UCPprefix + ".groupConfig." + groupname);
                    return null;
                }
            }
            GM_log("did not find definitions for " + groupname);
            return null;
        }
    },
    readGroupConfigURL: function (groupname, synchronous, callback) {
        //GM_log("reading group url for '" + groupname + "' - synchronous: " + synchronous);
        if (synchronous) {
            this.readGrouplistURL(true, function (result) {
		if (result.stat == 'error') {
			GM_log(result.error);
			if ($chk(callback)) callback(result);
			return;
		}
	    });
        }
        var groupList = this.groupList();
        if (!$chk(groupList[groupname]) || !$chk(groupList[groupname].definitions)) {
            return;
        }
        var groupUrl = groupList[groupname].definitions;
        //GM_log("reading group definitions for '" + groupname + "' (" + groupList[groupname].groupId + ") from '" + groupUrl + "'");
        var request = new Request({
            url: groupUrl,
            async: !synchronous,
            onSuccess: function (responseText, responseXML) {
                var discussionHTML = responseText;
                var tempDiv = new Element('div', {
                    html: discussionHTML.stripScripts()
                });

                var problem = tempDiv.getElement('p.Problem');
                if ($chk(problem)) {
                        if ($chk(callback)) {
                            callback( { stat: 'error', 'error': problem.innerHTML } );
                        }
			return;
		}
                var announcement = tempDiv.getElement('td.Said p');
                announcement.getElements('small').each(function (small) { 
                    small.dispose(); 
                });
                var groupConfiguration = announcement.textContent
                                            .trim()
                                            .replace(/\n/g, '') // Flickr changes
                                            .replace(/\"/g, '&quot;')
                                            .replace(/\\\'/g, '&#39;') // replace \' with its html number
                                            .replace(/\'{2}/g, "\"\"")
                                            // make the definitions strict JSON (use ' instead of " -> no &quot;)
                                            .replace(/\'([^\']+)\'/g, '\"$1\"') // replace ' with " for valid JSON
                                            // reset
                                            .replace(/&quot;/g, "\'");
                //GM_log("groupConfiguration: " + groupConfiguration);
                var onlineConfig;
                try {
                    onlineConfig = JSON.parse(groupConfiguration);
                    // make the definitions json-valid (use ' instead of " -> no &quot;)
                } catch (e) {
                    GM_log("json error in group config def: " + e); // JSON can't handle 'reName': /^d.../
                    GM_log("onlineConfig: " + groupConfiguration);
                    // Chrome only uses JSON!!
                    try {
                        onlineConfig = eval('(' + groupConfiguration + ')');
                    } catch (e) {
                        GM_log("error evaluating onlineConfig: " + e);
                        GM_log("onlineConfig: " + groupConfiguration);
                        if ($chk(callback)) {
                            callback( { stat: 'error', 'error': 'error evaluating onlineConfig: ' + e } );
                        }
                        return;
                    }
                }
                //GM_log("preparing default states");
                // reset defaults for non-defined states
                // TODO: document
                var defaultStates = {
                    open: "OPEN", // no photos yet
                    waitingForEntries: "OPEN", // at least one photo; some groups use "ON HOLD", ..
                    vote: /VOTE/i, // some groups use "VOTING", ..
                    closed: "CLOSED", // 
                    expired: "EXPIRED",
                    voided: "VOIDED"
                };
                // inject group list items
                for (var item in groupList[groupname]) {
                    if (groupList[groupname].hasOwnProperty(item)) {
                        onlineConfig[item] = groupList[groupname][item];
                    }
                }
                if (!$chk(onlineConfig.states)) {
                    onlineConfig.states = defaultStates;
                    // special case: vote is defined as a regexp in the defaults => Chrome won't have a vote state!
                    onlineConfig.states.voteRegExp = {
                        expression: 'VOTE',
                        flags: 'i'
                    };
                } else {
                // warning: special case: vote is defined as a regexp in the defaults!
                    for (var state in defaultStates) {
                        if (defaultStates.hasOwnProperty(state)) {
                            if (!$chk(onlineConfig.states[state])) {
                                if (state === 'vote' && !$chk(onlineConfig.states['voteRegExp'])) {
                                    onlineConfig.states.voteRegExp = {
                                        expression: 'VOTE',
                                        flags: 'i'
                                    };
                                } else {
                                    onlineConfig.states[state] = defaultStates[state];
                                }
                            }
                        }
                    }
                }
                GM_storeObject(UCPprefix + ".groupConfig." + groupname, onlineConfig);
                GM_setValue(UCPprefix + ".groupConfig.lastReadTime." + groupname, new Date().getTime().toString());
                if ($chk(callback)) {
                    callback( { stat: 'ok' } );
                }
            }.bind(this),
	    onFailure: function (response) {
		GM_log("error reading group config url: " + response.statusText);
		if ($chk(callback)) {
			callback({ stat: 'error', error: 'error reading group config url: ' + response.statusText });
		}
	    }
        }).get();
    },
    readGrouplistURL: function (synchronous, callback) {
        //GM_log("reading group list");
        var request = new Request({
            method: 'get', 
            url: this.groupListingURL,
            async: !synchronous, 
            onSuccess: function (responseText, responseXML) {
                var discussionHTML = responseText;
                var tempDiv = new Element('div', {
                    html: discussionHTML.stripScripts()
                });
                
                var announcement = tempDiv.getElement('td.Said p');
                announcement.getElements('small').each(function (small) { 
                    small.dispose(); 
                });
                // turn it into strict JSON
                var groupList = announcement.textContent.trim().replace(/\n/g, '')
                                            // make the definitions strict JSON (use ' instead of " -> no &quot;)
                                            .replace(/\'([^\']+)\'/g, '\"$1\"'); // replace ' with " for valid JSON
                var groups = {};
                try {
                    groups = JSON.parse(groupList);
                } catch(e) {
                    GM_log("json error in group list: " + e);
                    GM_log("groupList: " + groupList);
                    try {
                        groups = eval('(' + groupList + ')');
                    } catch (e) {
                        GM_log("error parsing groupList result: " + e);
                        GM_log("groupList: " + groupList);
                        if ($chk(callback)) {
                            callback( { stat: 'error', error: 'error parsing groupList result: ' + e });
                        }
                        return;
                    }
                }
                // inject groupname
                $each(groups, function (value, key) {
                    value.groupname = key;
                });
                //ucpStoredGrouplist = groups;
                //GM_log("storing group list");
                GM_storeObject(UCPprefix + ".groupConfig.list", groups);
                if ($chk(callback)) {
                    callback( { stat: 'ok' } );
                }
            }.bind(this),
	    onFailure: function (response) {
		GM_log("error reading group list url: " + response.statusText);
		if ($chk(callback)) {
			callback({ stat: 'error', error: 'error reading groupList: ' + response.statusText });
		}
	    }
        }).send();
    },
    groupList: function () {
        if ($chk(GM_getValue(UCPprefix + ".groupConfig.list"))) {
            //GM_log("working with storage value, and update in background");
            try {
                return GM_getObject(UCPprefix + ".groupConfig.list");
            } catch (e) { // parse error?
                GM_deleteValue(UCPprefix + ".groupConfig.list");
                //return null; continue: reread
            }
        }
        //GM_log("reading list url");
        this.readGrouplistURL(true);
        //GM_log("returning stored dict: '" + ucpStoredGrouplist + "'");
        try {
            return GM_getObject(UCPprefix + ".groupConfig.list");
        } catch (e) { // parse error?
            GM_deleteValue(UCPprefix + ".groupConfig.list");
            return null;
        }
    }
});


var UCPMedalListReader = new Class({
    timeBetweenReads: 7 * 24 * 60 * 60 * 1000, // a week
    medalListingURL: 'http://www.flickr.com/groups/unified_checkplay/discuss/72157625730082807/',
    initialize: function () {
    },
    checkForUpdates: function (force, callback) {
        var list = GM_getValue("UCPA.medalList");
        if ($chk(list)) {
            var lastReadTime = GM_getValue("UCPA.medalList.lastReadTime");
            var now = new Date().getTime();
            var elapsedTime = $chk(lastReadTime) ? now - lastReadTime : this.timeBetweenReads + 1;
            if (elapsedTime > this.timeBetweenReads || force) {
                GM_log("updating medal definitions");
                this.readMedalsURL(force === true ? true : false, callback);
            }
        } else {
            this.readMedalsURL(false, callback);
        }
    },
    createMedalsList: function () {
        var storedList = GM_getValue("UCPA.medalList");
        if ($chk(storedList)) {
            try {
                return GM_getObject("UCPA.medalList");
            } catch (e) {
                // parse error?
                GM_deleteValue("UCPA.medalList");
                return null;
            }
        } else {
            // if not available, read from URL, synchronously
            //GM_log("reading medalListingURL synchronously");
            this.readMedalsURL(true);
            storedList = GM_getValue("UCPA.medalList");
            if ($chk(storedList)) {
                try {
                    return GM_getObject("UCPA.medalList");
                } catch (e) {
                    // parse error?
                    GM_deleteValue("UCPA.medalList");
                    return null;
                }
            }
            GM_log("did not find definitions for medals!!");
            return null;
        }
    },
    readMedalsURL: function (synchronous, callback) {
        GM_log("reading medals url - synchronous: " + synchronous);
        var request = new Request({
            url: this.medalListingURL,
            async: !synchronous,
            onSuccess: function (responseText, responseXML) {
                var discussionHTML = responseText;
                var tempDiv = new Element('div', {
                    html: discussionHTML.stripScripts()
                });

                var medalList = {};
                var medalListingParts = tempDiv.getElements('td.Said p').each(function (medalListingPart) {
                    medalListingPart.getElements('small, br').each(function (el) { 
                        el.dispose(); 
                    });
                    var medalListingPartHTML = medalListingPart.textContent
                                                .trim()
                                                .replace(/\n/g, '') // flickr changes
                                                .replace(/\"/g, '&quot;')
                                                .replace(/\\\'/g, '&#39;') // replace \' with its html number
                                                .replace(/\'{2}/g, "\"\"")
                                                // make the definitions strict JSON (use ' instead of " -> no &quot;)
                                                .replace(/\'([^\']+)\'/g, '\"$1\"') // replace ' with " for valid JSON
                                                // reset
                                                .replace(/&quot;/g, "\"");
                    var onlineMedalPart;
                    try {
                        onlineMedalPart = JSON.parse(medalListingPartHTML);
                        // make the definitions json-valid (use ' instead of " -> no &quot;)
                    } catch (e) {
                        GM_log("json error: " + e + " - trying with eval");
                        GM_log("onlineConfig: " + medalListingPartHTML);
                        try {
                            onlineMedalPart = eval('(' + medalListingPartHTML + ')');
                        } catch (e) {
                            GM_log("error evaluating onlineConfig: " + e);
                            GM_log("onlineConfig: " + groupConfiguration);
                            if ($chk(callback)) {
                                callback( { stat: 'error', 'error': 'error evaluating onlineConfig: ' + e } );
                            }
                            return;
                        }
                    }
                    $each(onlineMedalPart, function(group, idx) {
                        //GM_log("adding group " + idx);
                        medalList[idx] = group;
			$each(group, function (medal, medalId) { // inject medal id into object
				medal.medalId = medalId;
			});
                    });
                });
                GM_storeObject("UCPA.medalList", medalList);
                GM_setValue("UCPA.medalList.lastReadTime", new Date().getTime().toString());
                if ($chk(callback)) {
                    callback( { stat: 'ok' } );
                }
            }.bind(this)
        }).get();
    }
});

var UCPGroupConfig = new Class({
    Implements: [Options],
    options: {
    	requiredUCPversion: undefined,
	requiredUCPAversion: undefined,
        challengeDefinitions: {},
        groupLimit: -1,
        states: {},
        allowsPhotoInAnnouncement: false,
        nonPhotoImages: {},
        groupLimitLabelAddon: "",
        skipFirstReply: false, // FavesContest shows last winning score in first reply => Finished
        skipFirstTwoRepliesForVotes: false, // FavesContest have a second reply '... 25-50 faves...', recognized as a vote
        mandatoryGroupLabels: false,
        automaticVoteStart: true,
        languageOverrides: {},
        legacyLabels: null,
        groupname: null, // the part of the Flickr URL
        name: null, // the human name
        groupId: null,
        awardWinnersGroup: false, // only used in UCP Admin
        primaryGroup: null, // only used in UCP Admin
	bumpMessage: "", //<i>This is a comment to bump the thread to the top of the list</i>"
	workflow: undefined,
	usesTimeConstraints: false,
	challengeNumberingRegExp: null,
	challengeNumberingLabel: null, // for challenge groups that don't number: check for name format
	similarThemesRegExp: null,
	debug: false
    },
    initialize: function (options) {
        this.setOptions(options);
        // override states with RegExp's
        [ 'open', 'waitingForEntries', 'closed', 'vote', 'expired', 'voided' ].each(function (state) {
            // override states with RegExp's
            if ($chk(options.states[state + 'RegExp'])) {
                //GM_log("creating " + state + " regexp");
                this.options.states[state] = 
                    new RegExp(options.states[state + 'RegExp'].expression, options.states[state + 'RegExp'].flags);
            }
            if (!$chk(this.options.states[state])) {
                GM_log("!!! no definition for state '" + state + "' !!!");
            }
        }, this);
        if (this.options.debug == true || this.options.debug == 'true') {
            // force re-read
            GM_setValue(UCPprefix + ".groupConfig.lastReadTime." + this.groupname(), 1);
        }
    },
    // accessors
    groupname: function () {
        return this.options.groupname;
    },
    requiredUCPversion: function () {
    	return this.options.requiredUCPversion;
    },
    requiredUCPAversion: function () {
    	return this.options.requiredUCPAversion;
    },
    challengeDefinitions: function () {
        return this.options.challengeDefinitions;
    },
    groupLimit: function () {
        return this.options.groupLimit;
    },
    groupLimitLabelAddon: function () {
        return this.options.groupLimitLabelAddon;
    },
    states: function () {
        return this.options.states;
    },
    legacyLabels: function () {
        return this.options.legacyLabels;
    },
    skipFirstReply: function () {
        return this.options.skipFirstReply;
    },
    skipFirstTwoRepliesForVotes: function () {
        return this.options.skipFirstTwoRepliesForVotes;
    },
    hasLegacyLabels: function () {
        return $chk(this.options.legacyLabels);
    },
    mandatoryGroupLabels: function () {
        return this.options.mandatoryGroupLabels;
    },
    automaticVoteStart: function () {
        return this.options.automaticVoteStart;
    },
    languageOverrides: function () {
        return this.options.languageOverrides;
    },
    allowsPhotoInAnnouncement: function () {
        return this.options.allowsPhotoInAnnouncement;
    },
    nonPhotoImages: function () {
        return this.options.nonPhotoImages;
    },
    groupId: function () {
        return this.options.groupId;
    },
    awardWinnersGroup: function () {
        return this.options.awardWinnersGroup;
    },
    primaryGroup: function () {
        return this.options.primaryGroup;
    },
    bumpMessage: function () {
    	return this.options.bumpMessage;
    },
    workflow: function() {
	if ($chk(this.options.workflow) && this.options.workflow.stickyChallenges == undefined) {
		this.options.workflow.stickyChallenges = true; // if not defined: sticky
	}
    	return this.options.workflow;
    },
    reExcludeMatch: function () {
        if ($chk(this.reExcludeMatchRegExp)) {
            return this.reExcludeMatchRegExp;
        }

        if (!$chk(this.options.excludes) || !$chk(this.options.excludes.matches) || !$chk(this.options.excludes.indexes)) {
            return /[^.]/; // match nothing
        }

        if ($chk(this.options.excludes.matches.reMatchRegExp)) { // only one
            this.reExcludeMatchRegExp = new RegExp(this.options.excludes.matches.reMatchRegExp.expression,
                                                    this.options.excludes.matches.flags);
            return this.reExcludeMatchRegExp;
        }

        var reMatches = [];
        for (var name in this.options.excludes.matches) {
            if (this.options.excludes.matches.hasOwnProperty(name)) {
                reMatches.push(this.options.excludes.matches[name]);
            }
        }
//        GM_log("creating regexp with '" + reMatches.join("|") + "'");
        return new RegExp(reMatches.join("|"), "ig");
    },
    excludeReplyIndexes: function () {
        if (!$chk(this.options.excludes) || !$chk(this.options.excludes.matches) || !$chk(this.options.excludes.indexes)) {
            return [];
        }
        var retval = [];
        for (var index in this.options.excludes.indexes) {
            if (this.options.excludes.indexes.hasOwnProperty(index)) {
                retval.push(this.options.excludes.indexes[index]);
            }
        }
        //GM_log("debug: " + retval);
        return retval;
    },
    challengeNumberingRegExp: function () {
	var storedRegExp = GM_getValue('UCPA.CrossCheckChallengeNumberingRegExp.' + this.groupname());
	if ($chk(storedRegExp)) return storedRegExp;
	return this.options.challengeNumberingRegExp;
    },
    challengeNumberingLabel: function () {
    	if (this.options.challengeNumberingLabel) {
		return this.options.challengeNumberingLabel;
	}
	return 'check the challenge numbering';
    },
    similarThemesRegExp: function () {
    	var storedRegExp = GM_getValue('UCPA.CrossCheckSimilarThemesRegExp.' + this.groupname());
	if ($chk(storedRegExp)) return storedRegExp;
	else return this.options.similarThemesRegExp;
    },
    extractChallengeDefinition: function (challengeName) {
        // do not run .each()! must stop after the first match: priorities
        if (!$chk(this.challengeDefinitions())) {
            GM_log("no challenge definitions for this group");
            return new UCPChallengeDefinition({
                scoreType: "UNKNOWN"
            });
        }
        for (var name in this.challengeDefinitions()) {
            if (this.challengeDefinitions().hasOwnProperty(name)) {
                var challengeDef = this.challengeDefinitions()[name];
                // TMOACG still uses reName (and others)
                var nameIndication = challengeDef.reName; // challengeDef is a simple JSON object
                if ($chk(challengeDef.reNameRegExp)) {
                    if (!$chk(challengeDef.reNameRegExpObject)) {
                        challengeDef.reNameRegExpObject = 
                                new RegExp(challengeDef.reNameRegExp.expression, challengeDef.reNameRegExp.flags);
                    }
                    nameIndication = challengeDef.reNameRegExpObject;
                }
                if (!$chk(nameIndication)) {
                    GM_log("!! missing reName, or reNameRegExp !!");
                } else {
                    var theMatch = challengeName.match(nameIndication);
                    // DEBUG
                    //GM_log("comparing "+challengeName+" with "+nameIndication+": "+theMatch);
                    if ($chk(theMatch)) {
                        // inject name
                        challengeDef.name = name;
                        return new UCPChallengeDefinition(challengeDef);
                    }
                }
            }
        }
        // none found!
        GM_log("NOT FOUND: " + challengeName + " not found in definitions");
        return new UCPChallengeDefinition({
            scoreType: "UNKNOWN"
        });
    },
    skipChallenge: function (challengeName) {
        if (challengeName.match(this.states().closed))  {
            return true;
        }
        if (challengeName.match(this.states().expired)) {
            return true;
        }
        if (challengeName.match(this.states().voided))  {
            return true;
        }
        for (var name in this.challengeDefinitions()) {
            if (this.challengeDefinitions().hasOwnProperty(name)) {
                var challengeDef = this.challengeDefinitions()[name];
                var specialName = ( $chk(challengeDef.reNameRegExpObject) ? challengeDef.reNameRegExpObject : challengeDef.reName);
                if (challengeName.match(specialName)) {
                    return false;
                }
            }
        }
        if (challengeName.match(this.states().open))    {
            return false;
        }
        if (challengeName.match(this.states().waitingForEntries)) {
            return false;
        }
        if (challengeName.match(this.states().vote))    {
            return false;
        }
        return true;
    },
    usesTimeConstraints: function () {
           return this.options.usesTimeConstraints == true || this.options.usesTimeConstraints == 'true';
    },
    hasTimeConstrainedChallenges: function () {
    	for (var name in this.challengeDefinitions()) {
		if (this.challengeDefinitions().hasOwnProperty(name)) {
			var challengeDef = this.challengeDefinitions()[name];
			// challengeDef is a simple JSON object
			if ($chk(challengeDef.usesTimeConstraints)) {
				return true;
			}
		}
	}
	return false;
    }
});

var isGroupAdministratorOrModerator = function (userNsid, groupId) { // removed from UCPGroupConfig to allow bumping in non-challenge groups
        var asyncUpdate = true;
	if (!$chk(groupId)) {
		groupId = groupConfig.groupId();
	}
	var retval = {
		moderator: false,
		administrator: false,
		groupname: undefined
	};
        var administrator = GM_getValue("UCPA.groupAdministrator." + groupId + "." + userNsid);
	var moderator = GM_getValue("UCPA.groupModerator." + groupId + "." + userNsid);
	var groupname = GM_getValue("UCPA.groupname." + groupId);
        if (administrator == undefined || moderator == undefined || groupname == undefined) { // not in our data
	    GM_debug("no admod data found");
            asyncUpdate = false;
        } else {
            retval.moderator = (moderator == true || moderator == 'true');
	    retval.administrator = (administrator == true || administrator == 'true');
	    retval.groupname = groupname;
	    
            var lastReadTime = GM_getValue("UCPA.groupAdminOrMod." + groupId + "." + userNsid + ".lastReadTime");
            GM_debug("name: " + retval.groupname + "admin: " + retval.administrator + " - mod: " + retval.moderator + " - time: " + lastReadTime);
            if (!$chk(lastReadTime)) {
                //GM_debug("using API to retrieve admin info");
                asyncUpdate = false;
		retval.moderator = false;
		retval.administrator = false;
		retval.groupname = undefined;
            } else {
                // run this test only once every hour
                var now = new Date().getTime();
                var elapsedTime = now - lastReadTime;
                if (elapsedTime < 60 * 60 * 1000) {
                    return retval;
                }
            }
        }
        var magisterLudi = GM_getMagisterLudi();
        var apiData = {
            api_key: magisterLudi,
            auth_hash: GM_getAuthHash(),
            auth_token: GM_getAuthToken(),
            format: 'json',
            nojsoncallback: 1,
            method: 'flickr.groups.members.getList',
            group_id: groupId,
            membertypes: '3,4',
            per_page: 500 // one page should be sufficient?
        };
        new Request({
            url: "http://www.flickr.com/",
            async: asyncUpdate,
            onFailure: function (response) {
                retval.moderator = false;
		retval.administrator = false;
		
                GM_log("failed reading members from group: " + response.statusText);
            },
            onSuccess: function (responseText, responseXML) {
                retval.moderator = false;
		retval.administrator = false;
		
                var result;
                try {
                    result = JSON.parse(responseText);
                } catch (e) {
                    result = eval('(' + responseText + ')');
                }
                if (result.stat === 'fail') {
                    GM_log("failed reading members from group: " +result.code + " - " + result.message);
                    return;
                }
                var members = result.members;
                $each(members.member, function (member) {
                    if (member.nsid === userNsid) {
			GM_debug("member: " + member.membertype);
		    	if (member.membertype == 3) { // moderator
				retval.moderator = true;
			} else if (member.membertype == 4) { // administrator
				retval.administrator = true;
			}
                    }
                });
		GM_setValue("UCPA.groupModerator." + groupId + "." + userNsid, retval.moderator);
		GM_setValue("UCPA.groupAdministrator." + groupId + "." + userNsid, retval.administrator);
                GM_setValue("UCPA.groupAdminOrMod." + groupId + "." + userNsid + ".lastReadTime", 
                            new Date().getTime().toString());
            }
        }).get("/services/rest", apiData);

        var apiNameData = {
            api_key: magisterLudi,
            auth_hash: GM_getAuthHash(),
            auth_token: GM_getAuthToken(),
            format: 'json',
            nojsoncallback: 1,
            method: 'flickr.groups.getInfo',
            group_id: groupId
        };
        new Request({
            url: "http://www.flickr.com/",
            async: asyncUpdate,
            onFailure: function (response) {
                GM_log("failed reading info from group: " + response.statusText);
            },
            onSuccess: function (responseText, responseXML) {
                var result;
                try {
                    result = JSON.parse(responseText);
                } catch (e) {
                    result = eval('(' + responseText + ')');
                }
                if (result.stat === 'fail') {
                    GM_log("failed reading info from group: " +result.code + " - " + result.message);
                    return;
                }
		retval.groupname = result.group.name._content;
		GM_setValue("UCPA.groupname." + groupId, retval.groupname);
                GM_setValue("UCPA.groupAdminOrMod." + groupId + "." + userNsid + ".lastReadTime", 
                            new Date().getTime().toString());
            }
        }).get("/services/rest", apiNameData);

// !!DEBUG!!
//retval.administrator = true;
        return retval;
    }


var ucpDialogStyle = {
    background: '#BFBFBF',
    '-moz-border-radius': '1em',
    '-webkit-border-radius': '1em',
    '-khtml-border-radius': '1em',
    'border-radius': '1em',
    border: 'grey solid 1px'
};
var ucpDialogStyleString = 'background: #BFBFBF; -moz-border-radius: 1em; -webkit-border-radius: 1em; -khtml-border-radius: 1em; border-radius: 1em; border: grey solid 1px;';

var UCPThreadDecoratorFactory = new Class({
    initialize: function () {
    }
});

var UCPTopicListingThreadDecoratorFactory = new Class({
    Extends: UCPThreadDecoratorFactory,
    initialize: function () {
    },
    createStatusDecorator: function(newchlgstatus, ucpGroupPreferences) {
        return new UCPTopicListingThreadStatusDecorator();
    },
    createPhotoDecorator: function () {
        return new UCPEmptyDecorator();
    },
    createExcludeDecorator: function () {
        return new UCPExcludeReplyDecorator();
    }
});

var UCPTopicListingReplyDecoratorFactory = new Class({
    Extends: UCPThreadDecoratorFactory,
    initialize: function () {
    },
    createPhotoDecorator: function () {
        return new UCPPhotoReplyDecorator();
    },
    createVoteDecorator: function () {
        return new UCPVoteReplyDecorator();
    },
    createExcludeDecorator: function () {
        return new UCPEmptyDecorator();
    }
});

var UCPTopicListingThreadStatusDecorator = new Class({
    decorateThread: function (ucpThread) {
    // TODO
    }
});

var UCPEmptyDecoratorFactory = new Class({
    Extends: UCPThreadDecoratorFactory,
    initialize: function() {
    },
    createVoteDecorator: function () {
        return new UCPEmptyDecorator();
    }
});

var UCPWorkflowThreadDecoratorFactory = new Class({
    Extends: UCPThreadDecoratorFactory,
    initialize: function () {
    }
});

var UCPExcludeReplyDecorator = new Class({
    initialize: function () {
    },
    decorateThread: function (ucpThread) {
        var feedbackElement = ucpThread.feedbackElement();
        $each(ucpThread.excludedPlayers(), function (excludedPlayer) {
            new Element('p', {
                html: "excluded: " + excludedPlayer
            }).inject(feedbackElement);
        });
    }
});

var UCPEmptyDecorator = new Class({
    initialize: function () {
    },
    decorateReply: function () {
        // do nothing
    },
    decorateThread: function () {
        // do nothing
    }
});

var UCPPhotoReplyDecorator = new Class({
    initialize: function () {
    },
    decorateReply: function (ucpCompetitor) {
        var feedbackElement = ucpCompetitor.ucpThread().feedbackElement();
        var firstSmall = ucpCompetitor.node().getElement('small').get('html').split(/\.|\(/)[0];

        var commentAnchor = new Element('div', { 
            html: '(' + firstSmall + '): ' + ucpCompetitor.poster().username 
        }).inject(feedbackElement);
        // ucpCompetitor.checkForValidPoster(); not! already done in processing of collectVotes
        ucpCompetitor.options.comments.each(function (comment) {
            commentAnchor.adopt(
                new Element('span', {
                    html: " - " + comment.msg,
                    styles: {
                        color: comment.type === 'comment' ? '':
                               comment.type === 'warning' ? 'orange' :
                               ucpCompetitor.poster().admin ? 'orange' : 'red'
                    }
                })
            );
        });
    }
});

var UCPVoteReplyDecorator = new Class({
    initialize: function () {
    },
    decorateReply: function (ucpVote) {
    try {
        var feedbackElement = ucpVote.ucpThread().feedbackElement();
        var voteFeedback = new Element('div', {
            html: ucpVote.poster().username
        }).inject(feedbackElement);
        ucpVote.messages().each(function (message) {
            var color = message.type === 'error' ? 'red' : message.type === 'warning' ? 'orange' : '';
            new Element('span', {
                html: " - " + message.msg,
                styles: {
                    color: color
                }
            }).inject(voteFeedback);
        });
    } catch (e) {
        GM_log("error: " + e);
    }
    }
});

var UCPCommentReplyDecorator = new Class({
    Implements: Options,
    options: {
    },
    initialize: function (options) {
        //this.setOptions(options);
    },
    decorateReply: function (ucpComment) {
        var pageAnchor = ucpComment.node().getElements('small').getLast();
        if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
            pageAnchor = pageAnchor.getParent('div.ucpdiv');
        }
        if (!$chk(pageAnchor)) {
            pageAnchor = this.options.node;
        }
        if (ucpComment.ucpThread.challengeDefinition().scoreType() === "MEETANDGREET") {
            var message = "found a greeting from <b>" + ucpComment.poster().username;
        } else {
            if ($chk(ucpComment.options.comments) && ucpComment.options.comments.length > 0) {
                ucpComment.options.comments.each(function (comment) {
                    message = ($chk(message) ? " - " : "") + comment.msg; // TODO: type: error
                });
            } else {
                message = "found a regular comment (no photo, no votes)";
            }
            message = message + " from " + ucpComment.poster().username;
        }
        new Element('small', {
            html: "UCheckPlayNG: " + message + "<br/>"
        }).inject(new Element('div', { 'class': 'ucpdiv' }).inject(pageAnchor, 'after'));
    }
});

var UCPChallengeDefinition = new Class({
    Implements: [Options],
    options: {
        name: undefined,
        reName: /.*/,
        reNameRegExp: undefined,
        neededPhotos: -1,
        neededScore: -1,
        scoreType: undefined,
        countsToLimit: false,
        limitPerPlayer: -1,
        playerVoting: 'mayvote',
        scoresAdded: true,
        iconChallenge: false,
	thumbnailView: false, // TODO: provide wizard
	usesTimeConstraints: undefined,
	postDeadline: undefined, // TODO: provide wizard
	voteDeadline: undefined, // TODO: provide wizard
	equalWeights: false,
	workflow: undefined // admin script only
    },
    initialize: function (options) {
        this.setOptions(options);
    },
    name: function () {
        return this.options.name;
    },
    reName: function () {
        if ($chk(this.options.reNameRegExp)) {
            return this.options.reNameRegExp;
        }
        return this.options.reName;
    },
    neededPhotos: function () {
        return this.options.neededPhotos;
    },
    setNeededPhotos: function (n) {
        this.options.neededPhotos = n;
    },
    neededScore: function () {
        return this.options.neededScore;
    },
    setNeededScore: function (n) {
        this.options.neededScore = n;
    },
    scoreType: function () {
        return this.options.scoreType;
    },
    setScoreType: function (s) {
        this.options.scoreType = s;
    },
    countsToLimit: function () {
        return this.options.countsToLimit;
    },
    setCountsToLimit: function (b) {
        this.options.countsToLimit = b;
    },
    limitPerPlayer: function () {
        return this.options.limitPerPlayer;
    },
    setLimitPerPlayer: function (n) {
        this.options.limitPerPlayer = n;
    },
    playerVoting: function () {
        return this.options.playerVoting;
    },
    setPlayerVoting: function (p) {
        this.options.playerVoting = p;
    },
    scoresAdded: function () {
        return this.options.scoresAdded;
    },
    setScoresAdded: function (s) {
        this.options.scoresAdded = s;
    },
    iconChallenge: function () {
        return this.options.iconChallenge;
    },
    setIconChallenge: function (b) {
        this.options.iconChallenge = b;
    },
    thumbnailView: function() {
        return this.options.thumbnailView;
    },
    setThumbnailView: function(b) {
        this.options.thumbnailView = b;
    },
    usesTimeConstraints: function () {
    	return this.options.usesTimeConstraints;
    },
    setUsesTimeConstraints: function (u) {
    	this.options.usesTimeConstraints = u;
    },
    postDeadline: function () {
    	return this.options.postDeadline;
    },
    setPostDeadline: function (deadline) {
    	this.options.postDeadline = deadline;
    },
    voteDeadline: function () {
    	return this.options.voteDeadline;
    },
    setVoteDeadline: function (deadline) {
    	this.options.voteDeadline = deadline;
    },
    equalWeights: function() {
	return this.options.equalWeights;
    },
    workflow: function() {
    	if ($chk(this.options.workflow)) {
		return this.options.workflow;
	}
	return {}
    },
    toString: function () {
        return [ 'reName: ' + this.reName(), 
                 'neededPhotos: ' + this.neededPhotos(), 
                 'neededScore: ' + this.neededScore(), 
                 'scoreType: ' + this.scoreType() ].join(","); 
    },
    clone: function () {
        return new UCPChallengeDefinition(this.options);
    },
    nonChallengeType: function () {
        switch (this.scoreType()) {
        case "CHAT":
        case "SHOWROOM":
        case "GAME":
        case "MEETANDGREET":
        case "INFO":
        case "UNKNOWN":
            return true;
        default:
            return false;
        }
        return true;
    },
    originalChallengeDefinition: function () {
    	if (!$chk(this.options.originalChallengeDefinition)) {
		this.options['originalChallengeDefinition'] = this.clone();
	}
	return this.options.originalChallengeDefinition;
    },
    readChallengeDefinitionOverrides: function (announcementNode) {
	this.originalChallengeDefinition(); // create backup, if not available yet
        var overrides = announcementNode.getElements('img[alt*=UCPoverride]');
        var overridesDefined = false;
        if ($chk(overrides) && overrides.length > 0) {
            overrides.each(function (overrideImg) {
                var override = overrideImg.alt;

                var photosOverrideMatch        = override.match(/:photos:(-{0,1}\d+)$/);
                if (photosOverrideMatch) {
                    var neededPhotosOverride = parseInt(photosOverrideMatch[1], 10);
                    if (this.neededPhotos() !== neededPhotosOverride) {
                        overridesDefined = true;
                        this.setNeededPhotos(neededPhotosOverride);
                        if (this.neededPhotos() < 0) {
                            this.setNeededPhotos(65535);
                        }
                    }
                }
                var votesOverrideMatch         = override.match(/:votes:(-{0,1}\d+)$/);
                if (votesOverrideMatch) {
                    var neededScoreOverride = parseInt(votesOverrideMatch[1], 10);
                    if (this.neededScore() !== neededScoreOverride) {
                        overridesDefined = true;
                        this.setNeededScore(neededScoreOverride);
                        if (this.neededScore() < 0) {
                            this.setNeededScore(65535);
                        }
                    }
                }
                var scoreTypeOverrideMatch     = override.match(/:type:(.*)$/);
                if (scoreTypeOverrideMatch) {
                    var scoreTypeOverride = scoreTypeOverrideMatch[1];
                    if (this.scoreType() !== scoreTypeOverride) {
                        overridesDefined = true;
                        this.setScoreType(scoreTypeOverride);
                    }
                }
                var maxPhotosOverrideMatch     = override.match(/:max:(-{0,1}\d+)/);
                if (maxPhotosOverrideMatch) {
                    var limitPerPlayerOverride = parseInt(maxPhotosOverrideMatch[1], 10);
                    if (this.limitPerPlayer() !== limitPerPlayerOverride) {
                        overridesDefined = true;
                        this.setLimitPerPlayer(limitPerPlayerOverride);
                    }
                }
                var playerVotingOverrideMatch  = override.match(/:voting:(n\a|mustvote|mayvote|maynotvote)$/);
                if (playerVotingOverrideMatch) {
                    var playerVotingOverride = playerVotingOverrideMatch[1];
                    if (this.playerVoting() !== playerVotingOverride) {
                        overridesDefined = true;
                        this.setPlayerVoting(playerVotingOverride);
                    }
                }
                var countsToLimitOverrideMatch = override.match(/:grouplimit:(true|false)$/);
                if (countsToLimitOverrideMatch) {
                    var countsToLimitOverride = (countsToLimitOverrideMatch[1] === "true");
                    if (this.countsToLimit() !== countsToLimitOverride) {
                        overridesDefined = true;
                        this.setCountsToLimit(countsToLimitOverride);
                    }
                }
                var scoresAddedOverrideMatch   = override.match(/:added:(true|false)$/);
                if (scoresAddedOverrideMatch) {
                    var scoresAddedOverride = (scoresAddedOverrideMatch[1] === "true");
                    if (this.scoresAdded() !== scoresAddedOverride) {
                        overridesDefined = true;
                        this.setScoresAdded(scoresAddedOverride);
                    }
                }
                var iconChallengeOverrideMatch   = override.match(/:icons:(true|false)$/);
                if (iconChallengeOverrideMatch) {
                    var iconChallengeOverride = (iconChallengeOverrideMatch[1] === "true");
                    if (this.iconChallenge() !== iconChallengeOverride) {
                        overridesDefined = true;
                        this.setIconChallenge(iconChallengeOverride);
                    }
                }
		var thumbnailViewOverrideMatch = override.match(/:thumbnailview:(true|false)$/);
                if (thumbnailViewOverrideMatch) {
                    var thumbnailViewOverride = (thumbnailViewOverrideMatch[1] === "true");
                    if (this.thumbnailView() !== thumbnailViewOverride) {
                        overridesDefined = true;
                        this.setThumbnailView(thumbnailViewOverride);
                    }
                }
		var postDeadlineDefinitionMatch = override.match(/:postdeadline:(\d+)$/);
                if (postDeadlineDefinitionMatch) {
                    var postDeadlineDefinition = postDeadlineDefinitionMatch[1];
                    this.setPostDeadline(postDeadlineDefinition);
                }
                var voteDeadlineDefinitionMatch = override.match(/:votedeadline:(\d+)$/);
                if (voteDeadlineDefinitionMatch) {
                    var voteDeadlineDefinition = voteDeadlineDefinitionMatch[1];
                    this.setVoteDeadline(voteDeadlineDefinition);
                }
            }, this);
        }
        return overridesDefined;
    }
});

var ucpInsertionIdx = 0;

function ucpCreateChallengeThread(options) {
    if (!challengeGroup) {
    	return new UCPUnknownThread(options);
    }
    var groupConfig = options.groupConfig;
    var chlgname = options.chlgname;
    var challengeDefinition = groupConfig.extractChallengeDefinition(chlgname);
    options.challengeDefinition = challengeDefinition;
    switch (challengeDefinition.scoreType()) {
    case "CHAT":
        return new UCPChatThread(options);
    case "SHOWROOM":
        return new UCPShowroomThread(options);
    case "GAME":
        return new UCPGameThread(options);
    case "MEETANDGREET":
        return new UCPMeetAndGreetThread(options);
    case "INFO":
        return new UCPInformationThread(options);
    case "UNKNOWN":
        return new UCPUnknownThread(options);
    default:
        return new UCPChallengeThread(options);
    }
}

var UCPThread = new Class({
    Implements: [Options],
    options: {
        groupConfig: undefined,
        chlgname: undefined,
        feedbackElement: undefined,
        chlgstatus: "none",
        challengeDefinition: undefined,
        replies: 0,
        votingErrors: [],
	photoErrors: [],
        validVotingAsStored: true,
        excludedPlayers: [],
        url: undefined,
        topic: undefined,
        scoreSummary: undefined,
	cummulativeScore: undefined,
        lastLoadTime: undefined,
        decoratorFactory: undefined,
        replyDecoratorFactory: undefined,
	challengeAnnouncementNode: undefined // to get the 'edited this topic xx minutes ago' for cross-checking
    },
    initialize: function (options) {
        this.setOptions(options);
        var reTopicMatch = /.*flickr.com\/groups\/[^\/.]*\/discuss\/([0-9]+)/;
        if (this.options.topic === undefined) {
            this.options.topic = reTopicMatch.exec(this.options.url)[1];
        }
    },
    toString: function () {
        return  [ "url:          " + this.url(),
                  "group:        " + this.groupname(),
                  "topic:        " + this.topic(),
                  "chlgname:     " + this.challengeName(),
                  "scroreSummary:" + this.scoreSummary(),
                  "chlgstatus:   " + this.challengeStatus(),
                  "validVoting:  " + this.validVoting(),
                  "votingErrors: " + this.votingError(),
		  "photoErrors:  " + this.photoErrors(),
                  "lastLoadTime: " + this.lastLoadTime()].join('\n');
    },
    // accessors
    groupConfig: function () {
        return this.options.groupConfig;
    },
    challengeName: function () {
        return this.options.chlgname;
    },
    setChallengeName: function (name) {
        this.options.chlgname = name;
    },
    feedbackElement: function (element) {
        return this.options.feedbackElement;
    },
    setFeedbackElement: function (element) {
        this.options.feedbackElement = element;
    },
    challengeStatus: function () {
        return this.options.chlgstatus;
    },
    setChallengeStatus: function (newstatus) {
        this.options.chlgstatus = newstatus;
    },
    challengeDefinition: function () {
        return this.options.challengeDefinition;
    },
    challengeAnnouncementNode: function () {
    	return this.options.challengeAnnouncementNode;
    },
    setChallengeAnnouncementNode: function (node) {
    	this.options.challengeAnnouncementNode = node;
    },
    replies: function () {
        return this.options.replies;
    },
    setReplies: function (replies) {
        this.options.replies = replies;
    },
    validVoting: function () {
        return (this.options.votingErrors.length === 0);
    },
    validVotingAsStored: function () {
        return this.options.validVotingAsStored;
    },
    votingError: function () {
        return this.options.votingErrors.join(" | ");
    },
    addVotingError: function (error) {
        if ($chk(error)) {
            this.options.votingErrors.include(error);
        }
    },
    photoErrors: function () {
    	return this.options.photoErrors.join(" | ");
    },
    addPhotoError: function (error) {
    	if ($chk(error)) {
		this.options.photoErrors.include(error);
	}
    },
    addExcludedPlayer: function (username) {
        if ($chk(username)) {
            this.options.excludedPlayers.include(username);
        }
    },
    excludedPlayers: function () {
        return this.options.excludedPlayers;
    },
    isExcluded: function (username) {
        if ($chk(username)) {
            return this.options.excludedPlayers.contains(username);
        }
        return false;
    },
    findExcludesInDOMNode: function (node) {
        reExcludeMatch = this.groupConfig().reExcludeMatch();
        var excludedMatch = reExcludeMatch.exec(node.innerHTML);
        if ($chk(excludedMatch)) {
            for (var exclMatchIdx = 1, exclMatchLen = excludedMatch.length; exclMatchIdx < exclMatchLen; ++exclMatchIdx) {
                var excludedPlayer = excludedMatch[exclMatchIdx];
                if (excludedPlayer) {
                    // special cases: admin, admins, administrators, I, me: ignore
                    this.addExcludedPlayer(excludedPlayer.replace(/&amp;/g, '&').replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ').trim());
                }
            }
        }
    },
    url: function () {
        return this.options.url;
    },
    topic: function () {
        return this.options.topic;
    },
    scoreSummary: function () {
        return this.options.scoreSummary;
    },
    setScoreSummary: function (summ) {
        this.options.scoreSummary = summ;
    },
    cummulativeScore: function () {
    	return this.options.cummulativeScore;
    },
    setCummulativeScore: function (vote) {
    	this.options.cummulativeScore = vote;
    },
    lastLoadTime: function () {
        return this.options.lastLoadTime;
    },
    setLastLoadTime: function (time) {
        this.options.lastLoadTime = time;
    },
    store: function () {
        GM_storeObject(UCPprefix + "." + this.groupname() + "." + this.topic(), {
                chlgname:       this.challengeName(),
                chlgstatus:     this.challengeStatus(),
		postDeadline:   $chk(this.postDeadline) ? this.postDeadline() : undefined,
		voteDeadline:   $chk(this.voteDeadline) ? this.voteDeadline() : undefined,
                scoreSummary:   (this.scoreSummary() ? this.scoreSummary() : ""),
                replies:        this.replies(),
                votingError:    !this.validVoting(),
                lastLoadTime:   (this.lastLoadTime() ? this.lastLoadTime() : 0)
            });
    },
    retrieve: function () {
        var value = GM_getObject(UCPprefix + "." + this.groupname() + "." + this.topic());
        if (value) {
            this.options.chlgname = value.chlgname;
            this.setChallengeStatus(value.chlgstatus);
	    if ($chk(this.setPostDeadline))
	    	this.setPostDeadline(value.postDeadline);
	    if ($chk(this.setVoteDeadline))
	    	this.setVoteDeadline(value.voteDeadline);
            this.setScoreSummary(value.scoreSummary);
            this.setReplies(parseInt(value.replies, 10));
            this.options.validVotingAsStored = !value.votingError;
            this.setLastLoadTime(parseInt(value.lastLoadTime, 10));
        }
    },
    groupname: function () {
        return this.groupConfig().groupname();
    },
    updateStatus: function (value) {
    //GM_log("current status: " + this.options.chlgstatus + ", value: " + value);
        if (this.challengeStatus() === "UPDATING") {
            this.setChallengeStatus("none");
        }
        if (value === "closed") {
            this.setChallengeStatus("Closed");
            return;
        }
        if (value === "voided") {
            this.setChallengeStatus("Voided");
            return;
        }
	if (value === "expired") {
	    this.setChallengeStatus("Expired");
	    return;
	}
        if ((this.challengeStatus() === "none") && (value === "open")) {
            this.setChallengeStatus("Open");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "filled")) {
            this.setChallengeStatus("Filled");
            return;
        }
        if (value === "finished") {
            this.setChallengeStatus("Finished");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "Unknown")) {
            this.setChallengeStatus("Unknown");
            return;
        }
    },
    decoratorFactory: function () {
        return this.options.decoratorFactory;
    },
    replyDecoratorFactory: function () {
        return this.options.replyDecoratorFactory;
    },
    printStatus: function (ucpGroupPreferences, newchlgstatus) {
        if (!newchlgstatus || newchlgstatus.length === 0) {
            newchlgstatus = this.challengeStatus();
        }
        this.decoratorFactory().createStatusDecorator().decorateThread(this);
    },
    printExcludes: function (challengeAnnouncement) {
    try {
        if ($chk(this.options.excludedPlayers) && this.options.excludedPlayers.length > 0) {
            this.decoratorFactory().createExcludeDecorator(challengeAnnouncement).decorateThread(this);
        }
    } catch (e) {
        GM_log("error: " + e);
    }
    },
    checkStatus: function () {
        if (this.challengeName().match(this.groupConfig().states().closed)) {
            this.updateStatus("closed");
        }
        if (this.challengeName().match(this.groupConfig().states().voided)) {
            this.updateStatus("voided");
        }
	if (this.challengeName().match(this.groupConfig().states().expired)) {
	    this.updateStatus("expired");
	}
	if ((this.challengeStatus() === "none") && this instanceof UCPChallengeThread) {
        	if (this.filled()) {
            		this.updateStatus("filled");
		}
        	if (this.open()) {
            		this.setChallengeStatus("Open");
        	} 
        	if (this.waitingForEntries()) {
            		this.setChallengeStatus("Open");
        	}
	}
        if (  (this.challengeStatus() === "none") 
            && $chk(this.challengeName().match(this.groupConfig().states().vote))) {
            this.setChallengeStatus("--VOTE--");
        }
    },
    resetStatus: function () {
        this.setChallengeStatus("none");
        this.options.votingErrors = [];
	this.options.photoErrors = [];
    },
    loadthread: function(groupPreferences, processDiscussionTopicCallback) {
//        var ifModifiedSince = ucpThread.lastLoadTime() ? new Date(ucpThread.lastLoadTime() * 1000) : new Date(0);
        //maybe we could use if-modified-since header to improve performance
        var ucpThread = this;
        new Request({
            url: ucpThread.url(),
            async: true,
            headers: {
                "Accept": "text/html",                    // If not specified, browser defaults will be used.
//                "If-Modified-Since": ifModifiedSince.toUTCString() // does not work on flickr :(
            },
            onFailure: function (response) {
                GM_log("error loading " + ucpThread.challengeName() + ": " + response.statusText);
                ucpThread.resetStatus();
                ucpThread.setChallengeStatus("ERRORLOADING");
                ucpThread.store();
                processDiscussionTopicCallback({ success: false, msg: "error loading: " + response.statusText, ucpThread: this });
            },
            onSuccess: function (responseText, responseXML) {
                this.cancel(); // stop downloading images
                ucpThread.setLastLoadTime(Math.round((new Date()).getTime() / 1000));

                var challengeConfig = ucpThread.challengeDefinition();
                var tempDiv = new Element('div', {
                    html: responseText.stripScripts()
                });
                // getElement with id, or getElementById does not work on tempDiv !?
                var discussionTopic = tempDiv.getElement('div[id=DiscussTopic]');
                // multiple pages?
                // getElement with id, or getElementById does not work on tempDiv !?
                var goodStuff = tempDiv.getElement('td[id=GoodStuff]');
                try {
                    var nextButton = goodStuff.getElement('div.Paginator').getElement('a.Next');
                } catch (e) {
                    //GM_log("error getting Next button: " + e);
                }
                ucpThread.loadNextTopicPage($chk(nextButton) ? nextButton.href : null, 
                                discussionTopic, 
                                processDiscussionTopicCallback);
            } // onload
        }).get(); // xmlHttpRequest
    }, // loadthread
    loadNextTopicPage: function(nextPageUri, discussionTopic, processDiscussionTopicCallback) {
        var ucpThread = this;
        if (!nextPageUri) { // the last page
            try {
                processDiscussionTopicCallback({ success: true, discussionTopic: discussionTopic, ucpThread: this });
            } catch (e) {
                GM_log("error processing (callback): " + e);
            }
            try {
                ucpThread.store();
            } catch (e) {
                GM_log("error storing" + e);
            }
            // TODO: photo-summary for multipage voting
            //ucpThread.printStatus(groupPreferences); => prints score summary twice!
            return;
        }
        new Request({
            method: "get",
            url: nextPageUri,
            async: false,
            onFailure: function (response) {
                GM_log("error loading " + nextPageUri + ": " + response.statusText);
                ucpThread.setChallengeStatus("ERRORLOADING");
                processDiscussionTopicCallback({ result: false, msg: "error loading: " + response.statusText, ucpThread: this });
                ucpThread.store();
            },
            onSuccess: function (responseText, responseXML) {
                this.cancel(); // stop downloading images
                var discussionHTML = responseText;
                var tempDiv = new Element('div', {
                    html: discussionHTML.stripScripts()
                });
                // getElement with id, or getElementById does not work on tempDiv !?
                var tempTable = tempDiv.getElement('div[id=Main]').getElement('td[id=GoodStuff]').getElement('table.TopicReply');
                var tempBody = tempTable.getElement('tbody');
                var tempRows = tempBody.getElements('tr');
                var firstPageRepliesTableBody = $(discussionTopic).getElement('table.TopicReply tbody');
                firstPageRepliesTableBody.adopt(
                    new Element('tr').adopt(
                        new Element('td', { 
                            colSpan: 2, 
                            html: 'Next page:'
                        })
                    )
                );
                tempRows.each(function (row) {
                    row.dispose();
                    row.inject(firstPageRepliesTableBody);
                });
                // multiple pages?
                // only for challenge threads 
                // => wrong! multiple page games, showrooms would not have a comment form 
                // => wrongly considered close
                // getElement with id, or getElementById does not work on tempDiv !?
                var goodStuff = tempDiv.getElement('td[id=GoodStuff]');
                if ($chk(goodStuff.getElement('div.Paginator'))) {
                    var nextButton = goodStuff.getElement('div.Paginator').getElement('a.Next');
                }
                ucpThread.loadNextTopicPage($chk(nextButton) ? nextButton.href : null, 
                                discussionTopic, 
                                processDiscussionTopicCallback);
            }
        }).send();
    },
    collectVotes: function (challengeAnnouncement, challengeEntries) {
        ucpCollectVotes(this, 
            this.groupConfig().allowsPhotoInAnnouncement() ? challengeAnnouncement : null,
            challengeEntries
        );
    },
    isClosed: function (bodyElement) {
	if (!$chk(bodyElement)) {
		bodyElement = $$('body')[0];
	}
	var closed = $chk(bodyElement.getElement('p.Focus * a[href*=unlock]'));
	if (!closed) {
		return { closed: false };
	}
	var closer = bodyElement.getElement('p.Focus');
	return {
		closed: true,
		closer: closer
	};
    },
    isUnanimous: function () {
    	switch (this.challengeDefinition().scoreType()) {
		case 'HORIZONTAL':
		case 'VERTICAL':
			return this.votes().length == 0 ? false : this.votes()[this.votes().length - 1].isUnanimous();
		case 'PIC-V-1':
			var unanimous = undefined;
			/* this.votes().each( function (vote) {
				GM_log("DEBUG: vote=" + vote);
			}); TODO: implement :-)
			*/
			return false;
		case 'PIC-P-1':
			// TODO
		default: 
			return false; // PIC-P-n, PIC-V-n, PIC-H-n with n > 1 are never anonimous
	}
					// same for RATE-PHOTO, VERTICAL-WEIGHTED
    }
});

var UCPNonChallengeThread = new Class({
    Extends: UCPThread,
    initialize: function (options) {
        this.parent(options);
    },
    printStatus: function (groupPreferences, ucpLanguage, newchlgstatus) {




















    },
    getLabelPrefix: function () {},

    collectVotes: function () {
    	return { votes: [], comments: [], photos: [] };
    },
    photos: function () {
	return [];
    },
    votes: function () {
	return [];
    },
    comments: function () {
	return [];
    }
});


var UCPChatThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Chat");
    },
    getLabelPrefix: function () {
        return "chat";
    }
});

var UCPShowroomThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Showroom");
    },
    getLabelPrefix: function () {
        return "showroom";
    }
});

var UCPGameThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Game");
    },
    getLabelPrefix: function () {
        return "game";
    }
});

var UCPMeetAndGreetThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("MeetAndGreet");
    },
    getLabelPrefix: function () {
        return "meetAndGreet";
    }
});

var UCPInformationThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Info");
    },
    getLabelPrefix: function () {
        return "info";
    }
});

/*var UCPVoidedThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Voided");
    },
    getLabelPrefix: function () {
        return "ignore";
    }
});*/

var UCPUnknownThread = new Class({
    Extends: UCPNonChallengeThread,
    initialize: function (options) {
        this.parent(options);
    },
    resetStatus: function () {
        this.parent();
        this.setChallengeStatus("Unknown");
    },
    getLabelPrefix: function () {
        return  "ignore";
    }
});

var UCPChallengeThread = new Class({
    Extends: UCPThread,
    initialize: function(options) {
        this.parent(options);
    },
    filled: function () {
        if (this.challengeDefinition().neededPhotos() <= 0) {
	    if (this.challengeName().match(this.groupConfig().states().open) || this.challengeName().match(this.groupConfig().states().waitingForEntries)) {
		if ($chk(this.postDeadline()) || this.usesTimeConstraints()) {
		    var flickrClock = calculateFlickrClock();
		    if ($chk(flickrClock)) {
			if ($chk(this.postDeadline())) {
				var postDeadlineMillis = this.postDeadline();
			} else {
				var filledRegExp = this.groupConfig().states().filledRegExp;
				if (!$chk(filledRegExp) || !$chk(filledRegExp.replacement_function)) {
					GM_log("ERROR: missing 'filledFunction'");
					return false;
				}
				postDeadlineMillis = this.challengeName().replace(new RegExp(filledRegExp.expression, filledRegExp.flags),
									new Function(filledRegExp.replacement_function));
				this.setPostDeadline(postDeadlineMillis);
			}
			var timeleft = postDeadlineMillis - flickrClock.getTime();
			if (timeleft <= 0) {
				return true;
			}
			return false;
		    }
		    return false;
		}
	    }
            return false;
        }
        if (this.photos().length >= this.challengeDefinition().neededPhotos() &&
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        return false;
    },
    open: function () {
        //GM_log("checking name against open states: " + this.chlgname + " - " + states.open + " - " + states.waitingForEntries);
        if (this.photos().length === 0 && 
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        return false;
    },
    waitingForEntries: function () {
        if (this.photos().length == 0) {
            return false;
        }
        if (this.challengeDefinition().neededPhotos() < 0 &&
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        if (this.challengeDefinition().neededPhotos() < 0) {
            return false;
        }
        return (this.photos().length < this.challengeDefinition().neededPhotos()
                && (this.challengeName().match(this.groupConfig().states().waitingForEntries) ||
                    this.challengeName().match(this.groupConfig().states().open)));
    },
    finished: function(vote) {
    	if (!$chk(vote)) { // vote is normally the cummulative score
		if (this.votes().length == 0) {
			return false;
		}
		vote = this.votes()[this.votes().length - 1];
	}
        // special case: PIC-P requires a diff of 2 points
        if (this.challengeDefinition().scoreType().match(/PIC-P-/)) {
            if (vote.topDiff() >= 2 &&
                this.challengeDefinition().scoresAdded() && 
                vote.maxVote >= this.challengeDefinition().neededScore()) {
                return true;
            }
            return false;
        }
        if (this.challengeDefinition().neededScore() === -1) {
	    if (this.challengeName().match(this.groupConfig().states().vote)) {
	    	if (this.usesTimeConstraints() || $chk(this.voteDeadline()) ) {
	    	    var flickrClock = calculateFlickrClock();
		    if ($chk(flickrClock)) {
		        if ($chk(this.voteDeadline())) {
			    var voteDeadlineMillis = this.voteDeadline();
		        } else {
			    var finishedRegExp = this.groupConfig().states().finishedRegExp;
			    if (!$chk(finishedRegExp) || !$chk(finishedRegExp.replacement_function)) {
				GM_log("ERROR: missing 'finishedFunction'");
				return false;
			    }
			    voteDeadlineMillis = this.challengeName().replace(new RegExp(finishedRegExp.expression, finishedRegExp.flags),
									new Function(finishedRegExp.replacement_function));
			    this.setVoteDeadline(voteDeadlineMillis);
		        }
		        var timeleft = voteDeadlineMillis - flickrClock.getTime();
		        if (timeleft <= 0) {
			    return true;
		        }
		        return false;
		    } 
		    return false;
	        }
                return false;
	    }
	    return false;
        }
        if (this.challengeDefinition().scoresAdded() && 
            vote.maxVote >= this.challengeDefinition().neededScore()) {
            return true;
        }
        if (!this.challengeDefinition().scoresAdded() && vote.isUnanimous()) {
            return true;
        }
        return false;
    },
    resetStatus: function() {
        this.parent();
        this.checkStatus();
    },
    hasError: function () {
	if (this.votes().some(function(vote) {
			return vote.hasError();
		})) {
		return true;
	}
	if (this.photos().some(function(photo) {
			return photo.hasError();
		})) {
		return true;
	}
	return false;
    },
    hasWarning: function () {
	if (this.votes().some(function(vote) {
			return vote.hasWarning();
		})) {
		return true;
	}
	if (this.photos().some(function(photo) {
			return photo.hasWarning();
		})) {
		return true;
	}
	return false;
    },
    photos: function () {
	if (this.photosArray == undefined) {
		return [];
	}
	return this.photosArray;
    },
    votes: function () {
	if (this.votesArray == undefined) {
		return [];
	}
	return this.votesArray;
    },
    comments: function () {
	if (this.commentsArray == undefined) {
		return [];
	}
	return this.commentsArray;
    },
    error: function () {
	var retval = "";
	this.photos().each(function (photo) {
		if (photo.hasError()) {
		    if (retval.length > 0) retval += " - ";
		    retval += photo.poster().username + ": " + photo.error()
		}
	});
	this.votes().each(function (vote) {
		if (vote.hasError()) {
		    if (retval.length > 0) retval += " - ";
		    retval += vote.poster().username + ": " + vote.error()
		}
	});
	return retval;
    },
    warning: function () {
	var retval = "";
	this.photos().each(function (photo) {
		if (photo.hasWarning()) {
		    if (retval.length > 0) retval += " - ";
		    retval += photo.poster().username + ": " + photo.warning()
		}
	});
	this.votes().each(function (vote) {
		if (vote.hasWarning()) {
		    if (retval.length > 0) retval += " - ";
		    retval += vote.poster().username + ": " + vote.warning()
		}
	});
	return retval;
    },
    usesTimeConstraints: function () {
    	if ($chk(this.challengeDefinition().usesTimeConstraints())) {
		return this.challengeDefinition().usesTimeConstraints();
	}
	return this.groupConfig().usesTimeConstraints();
    },
    setUsesTimeConstraints: function (u) {
    	this.challengeDefinition().setUsesTimeConstraints(u);
    },
    postDeadline: function () {
        return this.challengeDefinition().postDeadline();
    },
    setPostDeadline: function (millis) {
        this.challengeDefinition().setPostDeadline(millis);
    },
    voteDeadline: function () {
        return this.challengeDefinition().voteDeadline();
    },
    setVoteDeadline: function (millis) {
        this.challengeDefinition().setVoteDeadline(millis);
    }
});

var UCPGroupPreferences = new Class({
    Implements: [Options],
    options: {
        groupConfig: null, // mandatory
        // UCheckPlay Admin preferences
        hideNonWonMedals: false,
        stopAtFirstFoundMedal: false,
        defaultAwardsGroup: null,
        defaultAwardsGroupInPendingItems: null,
        bumpAndReload: false,
        bumpAndReturnToFirstPage: false,
        smartPhotoBoxes: true,
	workflowAutoClaimOwnership: false,
	useLevenshteinToMarkUsedThemes: true,
	levenshteinDistanceInUsedThemes: 3,
	checkPhotoNumbering: true,
	checkPhotoNumberingGently: false,
	CrossCheckChallengeNumbering: 'default', // start with capital: double use
	CrossCheckSimilarThemes: 'default',
	CrossCheckCountPlayerEntries: 'default',
	CrossCheckMultipleChallenges: 'default',
	CrossCheckNonVoters: 'default'
    },
    initialize: function(options) {
        this.setOptions(options);
        // admin prefs
        var storedHideNonWonMedals = GM_getValue("UCPA.hideNonWonMedals." + this.groupConfig().groupname());
        this.options.hideNonWonMedals = (storedHideNonWonMedals === true || storedHideNonWonMedals === 'true');
        var storedStopAtFirstFoundMedal = GM_getValue("UCPA.stopAtFirstFoundMedal." + this.groupConfig().groupname());
        this.options.stopAtFirstFoundMedal = (storedStopAtFirstFoundMedal === true || storedStopAtFirstFoundMedal === 'true');
        var storedDefaultAwardsGroup = GM_getValue("UCPA.defaultAwardsGroup." + this.groupConfig().groupname());
        if ($chk(storedDefaultAwardsGroup)) {
            this.options.defaultAwardsGroup = storedDefaultAwardsGroup;
        } else {
            this.options.defaultAwardsGroup = this.groupConfig().groupname();
        }
        var storedDefaultAwardsGroupInPendingItems = GM_getValue("UCPA.defaultAwardsGroupInPendingItems." + this.groupConfig().groupname());
        if ($chk(storedDefaultAwardsGroupInPendingItems)) {
            this.options.defaultAwardsGroupInPendingItems = storedDefaultAwardsGroupInPendingItems;
        } else if (this.groupConfig().awardWinnersGroup()) {
            this.options.defaultAwardsGroupInPendingItems = this.groupConfig().primaryGroup();
        } else {
            this.options.defaultAwardsGroupInPendingItems = this.groupConfig().groupname();
        }
        var storedBumpAndReload = GM_getValue("UCPA.bumpAndReload." + this.groupConfig().groupname());
        this.options.bumpAndReload = (storedBumpAndReload === true || storedBumpAndReload === 'true');
        var storedBumpAndReturnToFirstPage = GM_getValue("UCPA.bumpAndReturnToFirstPage." + this.groupConfig().groupname());
        this.options.bumpAndReturnToFirstPage = (storedBumpAndReturnToFirstPage == true || storedBumpAndReturnToFirstPage == 'true');
        var storedSmartPhotoBoxes = GM_getValue("UCPA.smartPhotoBoxes." + this.groupConfig().groupname());
        this.options.smartPhotoBoxes = (storedSmartPhotoBoxes === undefined || storedSmartPhotoBoxes === true || storedSmartPhotoBoxes === 'true');
	// workflow prefs
	var storedWorkflowAutoClaimOwnership = GM_getValue("UCPA.workflowAutoClaimOwnership." + this.groupConfig().groupname());
	this.options.workflowAutoClaimOwnership = (storedWorkflowAutoClaimOwnership === true || storedWorkflowAutoClaimOwnership === 'true');
	// levenshtein
	var storedUseLevenshteinToMarkUsedThemes = GM_getValue("UCPA.useLevenshteinToMarkUsedThemes." + this.groupConfig().groupname());
	this.options.useLevenshteinToMarkUsedThemes = (storedUseLevenshteinToMarkUsedThemes === false || storedUseLevenshteinToMarkUsedThemes === 'false') ? false : true;
	var storedLevenshteinDistanceInUsedThemes = GM_getValue("UCPA.levenshteinDistanceInUsedThemes." + this.groupConfig().groupname());
	if ($chk(storedLevenshteinDistanceInUsedThemes)) {
		this.options.levenshteinDistanceInIsedThemes = storedLevenshteinDistanceInUsedThemes;
	}
	// cross inspection
	var storedCrossCheckChallengeNumbering = GM_getValue("UCPA.CrossCheckChallengeNumbering." + this.groupConfig().groupname());
	if ($chk(storedCrossCheckChallengeNumbering)) {
		this.setCrossCheckChallengeNumbering(storedCrossCheckChallengeNumbering);
	}
	var storedCrossCheckSimilarThemes = GM_getValue("UCPA.CrossCheckSimilarThemes." + this.groupConfig().groupname());
	if ($chk(storedCrossCheckSimilarThemes)) {
		this.setCrossCheckSimilarThemes(storedCrossCheckSimilarThemes);
	}
	var storedCrossCheckCountPlayerEntries = GM_getValue("UCPA.CrossCheckCountPlayerEntries." + this.groupConfig().groupname());
	if ($chk(storedCrossCheckCountPlayerEntries)) {
		this.setCrossCheckCountPlayerEntries(storedCrossCheckCountPlayerEntries);
	}
	var storedCrossCheckMultipleChallenges = GM_getValue("UCPA.CrossCheckMultipleChallenges." + this.groupConfig().groupname());
	if ($chk(storedCrossCheckMultipleChallenges)) {
		this.setCrossCheckMultipleChallenges(storedCrossCheckMultipleChallenges);
	}
	var storedCrossCheckNonVoters = GM_getValue("UCPA.CrossCheckNonVoters." + this.groupConfig().groupname());
	if ($chk(storedCrossCheckNonVoters)) {
		this.setCrossCheckNonVoters(storedCrossCheckNonVoters);
	}
    },
    groupConfig: function () {
        return this.options.groupConfig;
    },
    setBumpAndReload: function (bumpAndReload) {
        if (bumpAndReload !== this.options.bumpAndReload) {
            GM_setValue("UCPA.bumpAndReload." + this.groupConfig().groupname(), bumpAndReload);
            return true;
        }
        return false;
    },
    setBumpAndReturnToFirstPage: function (b) {
        if (b !== this.options.bumpAndReturnToFirstPage) {
            GM_setValue("UCPA.bumpAndReturnToFirstPage." + this.groupConfig().groupname(), b);
            return true;
        }
        return false;
    },
    setSmartPhotoBoxes: function (smartPhotoBoxes) {
        if (smartPhotoBoxes !== this.options.smartPhotoBoxes) {
            GM_setValue("UCPA.smartPhotoBoxes." + this.groupConfig().groupname(), smartPhotoBoxes);
            return true;
        }
        return false;
    },
    setWorkflowAutoClaimOwnership: function (workflowAutoClaimOwnership) {
    	if (workflowAutoClaimOwnership !== this.options.workflowAutoClaimOwnership) {
		GM_setValue("UCPA.workflowAutoClaimOwnership." + this.groupConfig().groupname(), workflowAutoClaimOwnership);
		return true;
	}
	return false;
    },
    setHideNonWonMedals: function (hideNonWonMedals) {
        if (hideNonWonMedals !== this.options.hideNonWonMedals) {
            GM_setValue("UCPA.hideNonWonMedals." + this.groupConfig().groupname(), hideNonWonMedals);
            return true;
        }
        return false;
    },
    setStopAtFirstFoundMedal: function (stopAtFirstFoundMedal) {
        if (stopAtFirstFoundMedal !== this.options.stopAtFirstFoundMedal) {
            GM_setValue("UCPA.stopAtFirstFoundMedal." + this.groupConfig().groupname(), stopAtFirstFoundMedal);
            return true;
        }
        return false;
    },
    setDefaultAwardsGroup: function (groupname) {
        if (groupname !== this.options.defaultAwardsGroup) {
            GM_setValue("UCPA.defaultAwardsGroup." + this.groupConfig().groupname(), groupname);
            return true;
        }
        return false;
    },
    setDefaultAwardsGroupInPendingItems: function (groupname) {
        if (groupname !== this.options.defaultAwardsGroupInPendingItems) {
            GM_setValue("UCPA.defaultAwardsGroupInPendingItems." + this.groupConfig().groupname(), groupname);
            return true;
        }
        return false;
    },
    setUseLevenshteinToMarkUsedThemes: function (b) {
	if (b != this.options.useLevenshteinToMarkUsedThemes) {
		GM_setValue("UCPA.useLevenshteinToMarkUsedThemes." + this.groupConfig().groupname(), b);
		return true;
	}
	return false;
    },
    setLevenshteinDistanceInUsedThemes: function (d) {
	if (d != this.options.levenshteinDistanceInUsedThemes) {
		GM_setValue("UCPA.levenshteinDistanceInUsedThemes." + this.groupConfig().groupname(), d);
		return true;
	}
	return false;
    },
    hideNonWonMedals: function () {
        return this.options.hideNonWonMedals;
    },
    stopAtFirstFoundMedal: function () {
        return this.options.stopAtFirstFoundMedal;
    },
    defaultAwardsGroup: function () {
        return this.options.defaultAwardsGroup;
    },
    defaultAwardsGroupInPendingItems: function () {
        return this.options.defaultAwardsGroupInPendingItems;
    },
    bumpAndReload: function () {
        return this.options.bumpAndReload;
    },
    bumpAndReturnToFirstPage: function () {
        return this.options.bumpAndReturnToFirstPage;
    },
    smartPhotoBoxes: function () {
        return this.options.smartPhotoBoxes;
    },
    workflowAutoClaimOwnership: function () {
    	return this.options.workflowAutoClaimOwnership;
    },
    useLevenshteinToMarkThemesInUse: function () {
	return this.options.useLevenshteinToMarkThemesInUse;
    },
    levenshteinDistanceInUsedThemes: function () {
	return this.options.levenshteinDistanceInUsedThemes;
    },
    checkPhotoNumbering: function () {
        return this.options.checkPhotoNumbering == true || this.options.checkPhotoNumbering == "true";
    },
    checkPhotoNumberingGently: function () {
        return this.options.checkPhotoNumberingGently == true || this.options.checkPhotoNumberingGently == "true";
    },
    privateSetCrossCheckValue: function (s, name) {
    	switch (s) {
		case 'always':
		case 'never':
		case 'default':
			this.options[name] = s;
			GM_setValue("UCPA." + name + "." + groupConfig.groupname(), s);
			break;
		default:
			this.options[name] = 'default';
		}
    },
    crossCheckChallengeNumbering: function () {
    	return this.options.CrossCheckChallengeNumbering;
    },
    setCrossCheckChallengeNumbering: function (s) {
    	this.privateSetCrossCheckValue(s, "CrossCheckChallengeNumbering");
    },
    crossCheckSimilarThemes: function () {
    	return this.options.CrossCheckSimilarThemes;
    },
    setCrossCheckSimilarThemes: function (s) {
    	this.privateSetCrossCheckValue(s, "CrossCheckSimilarThemes");
    },
    crossCheckCountPlayerEntries: function () {
    	return this.options.CrossCheckCountPlayerEntries;
    },
    setCrossCheckCountPlayerEntries: function (s) {
    	this.privateSetCrossCheckValue(s, "CrossCheckCountPlayerEntries");
    },
    crossCheckMultipleChallenges: function () {
    	return this.options.CrossCheckMultipleChallenges;
    },
    setCrossCheckMultipleChallenges: function (s) {
    	this.privateSetCrossCheckValue(s, "CrossCheckMultipleChallenges");
    },
    crossCheckNonVoters: function () {
    	return this.options.CrossCheckNonVoters;
    },
    setCrossCheckNonVoters: function (s) {
    	this.privateSetCrossCheckValue(s, "CrossCheckNonVoters");
    }
});

function ucpCheckPhoto(photoNode, index, array) { // expects 'this' to be of type UCPChallengeDefinition
    try {
	    var src = photoNode.getAttribute('src');
        if (!$chk(src) || src.length === 0) {
            return false;
        }
	try {
            if (photoNode.getAttribute('alt') === 'UCPthumbnail') {
                return false;
            }
	    if (photoNode.getAttribute('alt').match('UCPANG:bump')) {
	        return false;
	    }
	} catch (e) {
	    // ignore
	}
        if (src.contains("buddyicons")) {
            return false;
        } 
        if (!src.match(/static\.?flickr\.com/)) {
            return false;
        }
        if (!this.iconChallenge()) {
            // ignore the height attribute: Flickr adds class="notsowide":
            // img.notsowide: {
            //     height: auto;
            //     max-width: 500px;
            //  }
            //  => even if specified, 'height' does nothing
	    try {
                var width = photoNode.getAttribute('width');
                // this.width returns bogus info if attribute 'width' is missing
                if ($chk(width) && width < 275) {
                    return false;
                }
	    } catch (e) {
	        // ignore
	    }
            // thumbnails, square, .. from flickr are not medium
            if (src.match(/_t.jpg$|_s.jpg$/)) {
                return false;
            }
        }
        if ($chk(groupConfig)) {
            if ($chk(groupConfig.nonPhotoImages()[src])) {
                return false;
            }
        }
        return true;
    } catch (e) {
        GM_log("error checking photo: " + e);
        return false;
    }
	return false;
}

function ucpAddCPheader() {
    if (challengeGroup) { // only show on challenge groups :=> bump
        new Element('span', {
            html: 'UNIFIED&nbsp;CP&nbsp;Admin&nbsp;' + CPtoolversion + '&nbsp;-&nbsp;',
            title: 'UNIFIED CheckPlay For Challenge Group Administration ' + CPtoolversion
        }).inject($("TopBar").getElement("td.Status"), 'top');
    }
}

function ucpCheckPendingPhoto (photoNode, index, array) {
    try {
	    var src = photoNode.src;
        if (!$chk(src) || src.length === 0) {
            return false;
        }
        if (photoNode.alt === 'UCPthumbnail') {
            return false;
        }
        if (src.contains("buddyicons")) {
            return false;
        } 
        if (!src.match(/static(?:\.)?flickr\.com/)) {
            return false;
        }
        return true;
    } catch (e) {
        GM_log(e);
        return false;
    }
	return false;
}

function ucpCheckBuddyicon (photoNode, index, array) {
    try {
        var src = photoNode.src;
        if (src.match(/http:\/\/.*flickr.com\/\d+\/buddyicons\/\d+@\w+.jpg/)) {
            return true;
        }
        // if none set, the default from flickr
        if (src.match(/http:\/\/.*\/buddyicon.(?:gif|jpg)#\d+@\w+/)) {
            return true;
        }
        // if a user is deleted, the default from flickr, without 
        var _class = photoNode.getAttribute('class');
        if (_class.match('xBuddyIconX') && src.match(/http:\/\/.*\/buddyicon.jpg$/)) {
            return true;
        }
    } catch (e) {
        GM_log(e);
        return false;
    }
    return false;
}

// from http://pmav.eu/stuff/javascript-hashing-functions/source.html
function ucpUniversalHash(s, tableSize) {
    if (!tableSize) {
        tableSize = 65534;
    }
    var b = 27183, h = 0, a = 31415;

    if (tableSize > 1) {
        for (i = 0; i < s.length; i++) {
            h = (a * h + s[i].charCodeAt()) % tableSize;
            a = ((a % tableSize) * (b % tableSize)) % (tableSize);
        }
    }
    return h;
}

var UCPVote = new Class({
    Implements: [Options],
    options: {
        chlgname: null,
        ucpThread: null,
        node: null,
        poster: null,
        voteText: null,
        votesArray: [],
        messages: [],
        errorIdx: null,
        votedFor: []
    },
    initialize: function(options) {
        this.setOptions(options);
        this.maxVote = 0;
        if (options.votesArray !== null) {
            for (var oIdx = 0, oLen = options.votesArray.length; oIdx < oLen; oIdx++) {
                if (!isNaN(options.votesArray[oIdx])) {
                    this.maxVote = Math.max(options.votesArray[oIdx], this.maxVote);
                    this.options.votesArray[oIdx] = options.votesArray[oIdx];
                } else {
                    this.options.votesArray[oIdx] = 0;
                }
            }
        }
    },
    poster: function () {
        return this.options.poster;
    },
    voteText: function () {
	return this.options.voteText;
    },
    node: function () {
        return this.options.node;
    },
    ucpThread: function () {
        return this.options.ucpThread;
    },
    isUnanimous: function () {
        var scoreIdx = 0;
        for (var oIdx = 1, oLen = this.options.votesArray.length; oIdx < oLen; ++oIdx) {
            if (!isNaN(this.options.votesArray[oIdx]) && this.options.votesArray[oIdx] > 0) {
                if (scoreIdx === 0) { // first vote found
                    scoreIdx = oIdx;
                } else {
                    if (scoreIdx !== oIdx) {
                        return false;
                    }
                }
            }
        }
        return scoreIdx !== 0; // no score found = no voting = not unanimous
    },
    isVoid: function () {
        return false;
    },
    add: function (other) {
        other.options.votesArray.each(function (vote, oIdx) {
            if (oIdx === 0) {
                return;
            }
            if (vote && !isNaN(vote)) {
                if (this.options.votesArray[oIdx] && !isNaN(this.options.votesArray[oIdx])) {
                    this.options.votesArray[oIdx] += vote;
                } else {
                    this.options.votesArray[oIdx] = vote;
                }
                if (this.options.votesArray[oIdx] > this.maxVote) {
                    this.maxVote = this.options.votesArray[oIdx];
                }
            }
        }, this);
    },
    topDiff: function () { // returns the difference between the two top scores
        var secondBest = 0;
        // this.maxVote should be set, but we don't have its index
        var theBest = 0;
        this.options.votesArray.each(function (score, oIdx) {
            if (oIdx === 0) {
                return;
            }
            if (score > theBest) {
                secondBest = theBest;
                theBest = score;
            } else if (score > secondBest) {
                secondBest = score;
            }
        }, this);
        return theBest - secondBest;
    },
    toString: function () {
        var retval = this.options.poster.username + ' ' + this.options.voteText + ' =(';
        this.options.votesArray.each(function (vote, idx) {
	    if (idx == 0) return;
            retval = retval + ' ' + vote;
        }, this);
        retval = retval + ') => ' + this.maxVote;
        return retval;
    },
    votedFor: function () {
        var votedForString;
        if (this.options.votedFor.length > 0) {
            votedForString = "(";
            this.options.votedFor.each( function(value,idx) {
                if (idx == 0) return;
                if (value != undefined && value > 0) {
                    if (votedForString.length > 1) votedForString += ",";
                    votedForString += (idx);
                }
            });
            votedForString += ")";
        }
        return votedForString;
    },
    showVotes: function (sort) {
        var retval = undefined;
        // create an array with objects {idx,vote}
        var showArray = this.options.votesArray.map( function (vote, idx) {
            return { photo: idx, points: vote };
        });
        if (sort) {
            // sort on points
            showArray.sort(function (a,b) {
                return b.points - a.points;
            });
        }
        showArray.each(function (vote, oIdx) {
            if (isNaN(vote.points) || vote.points <= 0) {
                return;
            }
            var part = vote.photo + ":" + vote.points + "pt";
            if (this.options.errorIdx === vote.photo) {
                part = "<b>" + part + "</b>";
            }
            if (retval === undefined) {
                retval = part;
            } else {
                retval = retval + ", "  + part;
            }
        }, this);
        return "(" + retval + ")";
    },
    showPicResult: function (sort) {
        // first, sort the result
        var sorted = [];
        var vote;
        for (var oIdx = 1, oLen = this.options.votesArray.length; oIdx < oLen; ++oIdx) {
            vote = this.options.votesArray[oIdx];
            if (!isNaN(vote)) {
                var added = false;
                for (var sIdx = 0, sLen = sorted.length; sIdx < sLen; ++sIdx) {
                    if (sorted[sIdx].value <= vote) {
                        // insert element => move the rest to the end
                        for (var rIdx = sorted.length; rIdx > sIdx; --rIdx) {
                            sorted[rIdx] = sorted[rIdx - 1];
                        }
                        sorted[sIdx] = { photo: oIdx, value: vote };
                        added = true;
                        break;
                    }
                }
                if (!added) {
                    sorted[sorted.length] = { photo: oIdx, value: vote };
                }
            }
        }
        var retval = "";
        sorted.each(function (vote) {
            retval = retval + (retval.length > 0 ? ", " : "") + vote.photo + ":" + vote.value;
        }, this);
        return "(" + retval + ")";
    },
    messages: function() {
        return this.options.messages;
    },
    addMessage: function (message) {
        this.options.messages.include({msg: message, type: 'message'});
    },
    addWarning: function (warning) {
        this.options.messages.include({msg: warning, type: 'warning'});
    },
    hasWarning: function () {
        return this.options.messages.some( function (msg) {
            return msg.type == 'warning';
        });
    },
    warning: function() {
        var retval = '';
        this.options.messages.each(function (message) {
            if (message.type === 'warning') {
		if (retval.length > 0) retval += " - ";
                retval += message.msg;
            }
        });
        return retval;
    },
    hasError: function () {
        return this.options.messages.some( function (msg) {
            return msg.type == 'error';
        });
    },
    addError: function (error) {
        this.options.messages.include({msg: error, type: 'error'});
    },
    error: function() {
        var retval = '';
        this.options.messages.each(function (message) {
            if (message.type === 'error') {
		if (retval.length > 0) retval += " - ";
                retval += message.msg;
            }
        });
        return retval;
    },
    printStatus: function (decoratorFactory) {
        if (!$chk(this.decorator)) {
            this.decorator = decoratorFactory.createVoteDecorator();
        }
        this.decorator.decorateReply(this);
    }
});

var UCPVerticalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) { 
        // numbers in vote == nPhotos ??
        var challengeConfig = this.ucpThread().challengeDefinition();
        if (challengeConfig.neededPhotos() > 0 && nPhotos == challengeConfig.neededPhotos()) {
            // we need an entry for each vote, numbered sequentially
            if (this.options.votesArray.length - 1 < nPhotos) {
                this.addError('did not provide a vote line for every photo');
                return false;
            }
            if (this.options.votesArray.length - 1 > nPhotos) {
                this.addError('provided too much vote lines');
                return false;
            }
        }
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        if (!previousVote || (voteCommentNumber == 1 && previousVote.isExampleVote())) { // first (real) vote
            // unless it is an example vote, the first vote entry should contain only 1 vote
            var nVotes = this.options.votesArray.filter( function(vote, idx) {
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this).length;
            if (nVotes > 1) {
                this.addError('voted on more than 1 photo');
                return false;
            }

            var maxOne = this.options.votesArray.every( function(vote, idx) {
                if (idx == 0) return true;
                if (vote == undefined) return true;
                if (vote > 1) return false;
                return true;
            });
            if (!maxOne) {
                this.addError('voted too much for the same photo');
                return false;
            }
        } else { // compare with previous vote: all votes should remain the same, except for one increment
	    nVotes = 0;
            this.options.votedFor = [];
            this.options.votesArray.each( function(vote, idx) {
                var prevVote = previousVote.options.votesArray[idx];
                if (idx == 0) return;
                if (vote == undefined && prevVote == undefined) return;
                if (prevVote != undefined) {
                    if (vote == undefined) {
                        this.addError('dropped a vote on photo ' + idx);
                    } else {
                        if (vote > ( prevVote + 1 )) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                            this.addError('voted too much on photo ' + idx);
                        } else if (vote < prevVote) {
                            this.addError('dropped a vote on photo ' + idx);
                        } else if (vote == (prevVote + 1)) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                        }
                    }
                } else {
                    if (vote > 1) {
		    	this.options.votedFor[idx] = vote;
			++nVotes;
                        this.addError('voted too much on photo ' + idx);
                    }
                }
            }, this);
	    if (this.options.votedFor.length == 0) {
                this.addError('did not really vote');
            }
	    if (nVotes > 1) {
	    	this.addError('voted for ' + nVotes + ' photos');
	    }
            if (!this.hasError()) {
                if (this.options.votesArray.length !== previousVote.options.votesArray.length) {
                    this.addError('but provided ' + (this.options.votesArray.length - 1) + ' vote lines while \'' + 
                            previousVote.poster().username + '\' provided ' + (previousVote.options.votesArray.length - 1));
                }
            }
            if (this.hasError()) return false;
        }
        
        return true;
    },
    isExampleVote: function () {
        // in VERTICAL, the example vote is empty
        return this.options.votesArray.every( function (vote, idx) {
            if (idx == 0) return true;
            return !vote || vote == 0;
        });
    }
});

var UCPHorizontalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        // numbers in vote == nPhotos ??
        var challengeConfig = this.ucpThread().challengeDefinition();
        if (challengeConfig.neededPhotos() > 0 && nPhotos == challengeConfig.neededPhotos()) {
            // we need an entry for each vote, numbered sequentially
            if (this.options.votesArray.length - 1 < nPhotos) {
                this.addError('did not provide a vote for every photo');
                return false;
            }
            if (this.options.votesArray.length - 1 > nPhotos) {
                this.addError('provided too much votes ');
                return false;
            }
        }
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        if (!previousVote || (voteCommentNumber == 1 && previousVote.isExampleVote()) &&
            this.ucpThread().challengeDefinition().scoresAdded()) { // first (real) vote
            // unless it is an example vote, the first vote entry should contain only 1 vote (0-0-1)
            var nVotes = this.options.votesArray.filter( function(vote, idx) {
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this).length;
            if (nVotes > 1) {
                this.addError('voted on more than 1 photo');
                return false;
            }
            var maxOne = this.options.votesArray.every( function(vote, idx) {
                if (idx == 0) return true;
                if (vote == undefined) return true;
                if (vote > 1) return false;
                return true;
            });
            if (!maxOne) {
                this.addError('voted too much for the same photo');
                return false;
            }
        } else { // compare with previous vote: all votes should remain the same, except for one increment/decrement
	    nVotes = 0;
            if (this.ucpThread().challengeDefinition().scoresAdded()) {
                this.options.votesArray.each( function(vote, idx) {
                    var prevVote = previousVote.options.votesArray[idx];
                    if (idx == 0) return;
                    if (vote == undefined && prevVote == undefined) return;
                    if (prevVote != undefined) {
                        if (vote == undefined) {
                            this.addError('dropped a vote on photo ' + idx);
                        } else {
                            if (vote > ( prevVote + 1 )) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                                this.addError('voted too much on photo ' + idx);
                            } else if (vote < prevVote) {
                                this.addError('dropped a vote on photo ' + idx);
                            } else if (vote == (prevVote + 1)) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                            }
                        }
                    } else {
                        if (vote > 1) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                            this.addError('voted too much on photo ' + idx);
                        }
                    }
                }, this);
            } else { // constructive challenges
                this.options.votesArray.each( function(vote, idx) {
                    var prevVote = previousVote.options.votesArray[idx];
                    if (idx == 0) return;
                    if (vote == undefined && prevVote == undefined) return;
                    if (prevVote != undefined) {
                        if (vote == undefined) {
                            this.addError('dropped a vote on photo ' + idx);
                        } else {
                            if (vote < ( prevVote - 1 )) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                                this.addError('voted too much on photo ' + idx);
                            } else if (vote > prevVote) {
                                this.addError('dropped a vote on photo ' + idx);
                            } else if (vote == (prevVote - 1)) {
                                this.options.votedFor[idx] = vote;
				++nVotes;
                            }
                        }
                    } else {
                        if (vote < 5) {
                            this.options.votedFor[idx] = vote;
			    ++nVotes;
                            this.addError('voted too much on photo ' + idx);
                        }
                    }
                }, this);
            }
	    if (this.options.votedFor.length == 0) {
                this.addError('did not really vote');
            }
	    if (nVotes > 1) {
	    	this.addError('voted for ' + nVotes + ' photos');
	    }
            if (!this.hasError()) {
                if (previousVote && this.options.votesArray.length !== previousVote.options.votesArray.length) {
                    this.addError('but provided ' + (this.options.votesArray.length - 1) + ' votes while \'' + 
                            previousVote.poster().username + '\' provided ' + (previousVote.options.votesArray.length - 1));
                }
            }
            if (this.hasError()) return false;
        }
        return true;
    },
    isExampleVote: function () {
        // in HORIZONTAL, the example vote is 1-2-3
        // unless in constructive challenges, which start at 5-5-5
        if (!this.ucpThread().challengeDefinition().scoresAdded()) {
            return this.options.votesArray.every( function (vote, idx) {
                if (idx == 0) return true;
                return vote == 5;
            });
        }
        return this.options.votesArray.every( function (vote, idx) {
            if (idx == 0) return true;
            return vote == idx;
        });
    }
});

var UCPVerticalWeightedVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) { 
        var challengeConfig = this.ucpThread().challengeDefinition();
        // numbers in vote == nPhotos ??
        if (challengeConfig.neededPhotos() > 0 && nPhotos == challengeConfig.neededPhotos()) {
            // we need an entry for each vote, numbered sequentially
            if (this.options.votesArray.length - 1 < nPhotos) {
                this.addWarning('did not provide a vote line for every photo');
            }
            if (this.options.votesArray.length - 1 > nPhotos) {
                this.addWarning('provided too much vote lines');
            }
        }
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        if (!previousVote || (voteCommentNumber == 1 && previousVote.isExampleVote())) { // first (real) vote
            // unless it is an example vote, the first vote entry should contain only 3 votes
            var nVotes = this.options.votesArray.filter( function(vote, idx) {
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this).length;
            if (nVotes < 3) {
                this.addError('voted on only ' + nVotes + ' photo' + (nVotes > 1 ? 's' : ''));
                return false;
            }
            if (nVotes > 3) {
                this.addError('voted on ' + nVotes + ' photos');
                return false;
            }

            // the first vote should contain 3 ratings: 1, 2 and 3
            var ratings = this.options.votesArray.filter( function(vote, idx) {
                if (idx == 0) return false;
                if (vote && vote > 0) {
                    this.options.votedFor[idx] = vote;
                    return true;
                }
                return false;
            }, this);
            if (ratings.length > 3) {
                this.addError('provided too much ratings');
                return false;
            }
            if (ratings.length < 3) {
                this.addError('did not provide enough ratings');
                return false;
            }
            var validRatings = this.options.votesArray.filter( function(vote, idx) {
                if (idx == 0) return false;
                if (vote && (vote == 3 || vote == 2 || vote == 1)) return true;
                return false;
            });
            // there can not be more than 3: already filtered in above test
            if (validRatings.length < 3) {
                this.addError('did not provide enough valid ratings');
                return false;
            }
            // check for duplicates
            validRatings.sort(function(a,b){return a - b});
            if (!validRatings.every(function (value, idx) { return value == idx + 1; })) {
                this.addError('provided duplicate ratings');
                return false;
            }
        
        } else { // compare with previous vote: all votes should remain the same, except for 3 ratings
            var voteArray = [];
            voteArray[1] = voteArray[2] = voteArray[3] = 0;
            this.options.votesArray.each( function(vote, idx) {
                if (idx == 0) return;
                var prevVote = previousVote.options.votesArray[idx];
                if ((vote == undefined || vote == 0) && (prevVote == undefined || prevVote == 0)) return;
                if (prevVote == undefined || prevVote == 0) { // first vote on this photo
                    this.options.votedFor[idx] = vote;
                    if (vote > 3) {
                        this.addError('voted too much on photo ' + idx);
                        return;
                    }
                    if (vote < 1) {
                        this.addError('voted negative on photo ' + idx);
                        return;
                    }
                    voteArray[vote] = ++voteArray[vote];
                } else {
                    if (prevVote == vote) return;
                    // new ratings on this one
                    voteArray[vote - prevVote] = ++voteArray[vote - prevVote];
                    this.options.votedFor[idx] = vote - prevVote;
                    if ((vote - prevVote) > 3) {
                        this.addError('voted too much on photo ' + idx);
                        return;
                    }
                    if (prevVote > vote) {
                        this.addError('dropped a vote on photo ' + idx);
                        return;
                    }
                }
            }, this);
            if (!this.hasError()) {
                if (this.options.votesArray.length !== previousVote.options.votesArray.length) {
                    this.addWarning('but provided ' + (this.options.votesArray.length - 1) + ' vote lines while \'' + 
                            previousVote.poster().username + '\' provided ' + (previousVote.options.votesArray.length - 1));
                }
            }
            if (!voteArray.every(function(vote,idx){
                if (idx == 0) return true;
                return vote == 1;
            })) {
                this.addError('invalid rating');
                // TODO: show doubles or missing ones
                GM_log("invalid rating: " + voteArray);
            }
        }
        
        return !this.hasError();
    },
    isExampleVote: function () {
        // in VERTICAL-WEIGHTED, the example vote is empty
        return this.options.votesArray.every( function (vote, idx) {
            if (idx == 0) return true;
            return !vote || vote == 0;
        });
    },
    votedFor: function () {
        // the same as for the base class, but the weight should show
        var votedForString;
        if (this.options.votedFor.length > 0) {
            votedForString = "(";
            this.options.votedFor.each( function(value,idx) {
                if (idx == 0) return;
                if (value != undefined && value > 0) {
                    if (votedForString.length > 1) votedForString += ",";
                    votedForString += (idx + ":" + value + "pt");
                }
            });
            votedForString += ")";
        }
        return votedForString;
    }
});

var UCPRatePhotoVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = vote;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        // 1 - 3pt
        // 2 - 2pt
        // 3 - 3pt
        // => votesArray (0,3,2,1)
        return this.options.votesArray.every( function (value, idx) {
                if (idx == 0) return true;
                return (idx + value) == 4;
        });
    },
    votedFor: function () {
        // the same as for the base class, but the weight should show
        var votedForString;
        if (this.options.votedFor.length > 0) {
            votedForString = "(";
            this.options.votedFor.each( function(value,idx) {
                if (idx == 0) return;
                if (value != undefined && value > 0) {
                    if (votedForString.length > 1) votedForString += ",";
                    votedForString += (idx + ":" + value + "pt");
                }
            });
            votedForString += ")";
        }
        return votedForString;
    }
});

var UCPPICHorizontalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = 1;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        return false;
    }
});

var UCPPICVerticalVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = 1;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        return false;
    }
});

var UCPPICPlayerVote = new Class({
    Extends: UCPVote,
    valid: function (previousVote, nPhotos, voteCommentNumber) {
        this.options.votedFor = [];
        if (!previousVote && this.isExampleVote()) {
            return true;
        }
        this.options.votesArray.each( function(vote, idx) {
            if (vote && vote > 0) {
                this.options.votedFor[idx] = 1;
            }
        }, this);
            // none of the votes should be for a non-existing photo
            // => does not work: if photo 14 is removed from the challenge, later entries may keep their number
        return true;
    },
    isExampleVote: function () {
        return false;
    }
});

function ucpCheckPhotoApproval(photo, topic) {
// TODO: 
// - fails in case there two or three photo entries: 
// http://www.flickr.com/groups/thumbsup_challenges/discuss/72157625215597037/
        // returns
        // {
        //      approved: true/false,
        //      approver: string,
        //      version: number,
        //      checksum: true/false,
        //      photoChecksum: true/false,
        //      error: string
        //      photoId: string
        //  }

    var photoRow = photo.getParent('tr');
    var photoTextNode = photoRow.getElement('td.Said p'); 
    var photoId = photo.get('src').match(/https?:\/\/.*flickr.com\/\d+\/(\d+)_.*/)[1];
/*return {
        approved: true,
        approver: "todo",
        photoId: photoId
    }; */
    try {
        var approvedNode = photoTextNode.getElement("img[src=http://l.yimg.com/g/images/spaceout.gif][alt*=UCPAapproved]");
    } catch (e) {
        GM_log("error: " + e);
        return true;
    }
    if (!approvedNode) {
        return { approved: false };
    }
    var approvedString = approvedNode.get('alt');
    var ignore, name, photoChecksum, checksum, version;
    try {
        var approveMatch = /UCPAapproved:([^:]*):([^:]*):([^:]*):(\d+)/.exec(approvedString);
        ignore = approveMatch[0];
        name = decodeURIComponent(approveMatch[1]);
        photoChecksum = approveMatch[2];
        checksum = approveMatch[3];
        version = approveMatch[4];
    } catch (e) {
        return { approved: false,
                 error: "checksum has been modified!",
                 photoId: photoId
        };
    }
    if ($chk(ignore)) {
        var goodPhotoChecksum = ucpUniversalHash(photo.src);
        if (goodPhotoChecksum !== parseInt(photoChecksum, 10)) {
            return { approved: false,
                     version: version,
                     checksum: true,
                     photoChecksum: false,
                     error: "photo has been changed after approval!",
                     photoId: photoId
            };
        }
        var goodChecksum = ucpUniversalHash(photoId + name + topic);
        //GM_log("good: " + goodChecksum + " - photo: " + checksum);
        if (goodChecksum !== parseInt(checksum, 10)) {
            return { approved: false,
                     version: version,
                     checksum: false,
                     error: "checksum has been tampered with!",
                     photoId: photoId
            };
        }
        return { 
            approved: true,
            approver: name,
            version: version,
            checksum: true,
            photoChecksum: true,
            photoId: photoId
        };
    } else {
        return { approved: false, error: "failed to process UCPA approved entry"};
    }
}

var ucpReHorizontalVoteMatch;
function ucpGetReHorizontalVoteMatch() {
    // TODO: if challengeConfig.neededPhotos === ucpThread.photosposted => limit reMatch to nPhotos
    if (!ucpReHorizontalVoteMatch) {
	// xX: when a photo is removed once voting has started, it may be replaced with x: 1-2-x-0
        // when split with a character, it can be double digits
        var reVoteMatchSplit  = "(?:\\b|\\s)(\\d+|[xX]{1})(?:\\s*)?[^xX\\d]{1,2}(?:\\s*)?(\\d+|[xX]{1})"; // at least 2
        // when stuck to each other, it should be single digits
        var reVoteMatchJoined = "(?:\\b|\\s)(\\d{1}|[xX]{1})(\\d{1}|[xX]{1})"; // at least 2
                
        // limit the seperation characters to non-alphabet characters:
        for (var p = 1; p < 10; ++p) {
            reVoteMatchSplit  = reVoteMatchSplit  + "(?:\\s*)?(?:[^xX\\d]{1,2})?(?:\\s*)?(\\d+|[xX]{1})?";
            reVoteMatchJoined = reVoteMatchJoined + "(\\d{1}|[xX]{1})?";
        }
        reVoteMatchSplit  = reVoteMatchSplit  + "(?:\\s|\\b)";
        reVoteMatchJoined = reVoteMatchJoined + "(?:\\s|\\b)";
        ucpReHorizontalVoteMatch = new RegExp(reVoteMatchSplit + "|" + reVoteMatchJoined, "ig");
    }
    return ucpReHorizontalVoteMatch;
}

function parseVerticalVote(data) {
    var replytxt = data.replytxt
    var challengeEntry = data.challengeEntry;
    var ucpThread = data.ucpThread;
    var challengeConfig = ucpThread.challengeDefinition();
    var poster = data.poster;
    var exampleVote = data.exampleVote;

    var vVotesArray = [];
    var vVoteLines = replytxt.split(/<br>\s*/); // source code shows '<br />', DOM uses <br>?
    // voteLines is an array:
    // #01- 1
    // #02- 1+2+3
    // ..
    // others use 1:
    var reVoteMatch1 = /(\d{1,2})#/; // 01-
    var reVoteMatch2 = /(\d{1,2})#(\d+)/; // '01-1'
    var reVoteMatch3 = /(\d{1,2})#(\d+)([^0-9azA-Z](\d+))+/; // '01-1+2' && '01- 1+2+3'
    var vVotesFound = 0;
    var vVoided = false;
    // it misses the first votes: 01-1 or 06-1, but that's to be ignored :)
    for (var j = 0, vVoteLinesLength = vVoteLines.length; j < vVoteLinesLength; ++j) {
        var vVoteLine = vVoteLines[j];
        // remove any leading spaces
        vVoteLine = vVoteLine.replace(/^\s*/, '');
        // ignore lines that are comment text
        if (vVoteLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            continue;
        }
        // first seperate the photo number from the votes, in case it was seperated with a space
        var reVReplaceMatch = /#(\d{1,2})/;
        vVoteLine = vVoteLine.replace(reVReplaceMatch, "$1"); // remove leading #
        reVReplaceMatch = /(\d{1,2})([\-:+\. ]+|\.{2,3})/; // faves contest uses '...', adding it to [] creates havoc in other places, even with escaping (or escaping double)
        vVoteLine = vVoteLine.replace(reVReplaceMatch, "$1#"); // insert # after photonumber
        // remove spaces, but first replace spaces between votes, if any
        vVoteLine = vVoteLine.replace(/(\d)\s+(\d)/g, "$1+$2");
        vVoteLine = vVoteLine.replace(/\s/g, '');
        // remove any character before first vote in voteLine 00#-1+2
        vVoteLine = vVoteLine.replace(/(\d{1,2}#)[^0-9]*/, "$1");
        if (vVoteLine.match(/^\s*\d{1,2}\s*$/)) {
            vVoteLine = vVoteLine.replace(/^\s*(\d{1,2})\s*$/, "$1#0");
        }
        if (vVoteLine.match(/void/i) && !vVoteLine.match(/^\d/)) {
            // Skip replies where the vote has been voided
            vVoided = true;
            break;
        }
        var matchedString, photoNumber, photoScore;
        var verticalVotes = reVoteMatch3.exec(vVoteLine);
        if (verticalVotes) {
            photoNumber = verticalVotes[1];
            photoScore = verticalVotes[4];
            vVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(verticalVotes[4], 10);
            ++vVotesFound;
            continue;
        }
        verticalVotes = reVoteMatch2.exec(vVoteLine);
        if (verticalVotes) {
            photoNumber = verticalVotes[1];
            photoScore = verticalVotes[2];
            vVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++vVotesFound;
            continue;
        }
        verticalVotes = reVoteMatch1.exec(vVoteLine);
        if (verticalVotes) {
            photoNumber = verticalVotes[1];
            vVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = 0;
            ++vVotesFound;
            continue;
        }
        // thisvotes is still null => normal comment
    }       
    //GM_log(vVotesArray);
    if (vVoided) {
        var voidedVote = new UCPVote({
            poster: poster, 
            voteText: vVoteLines, 
            votesArray: vVotesArray, 
            node: challengeEntry,
            ucpThread: ucpThread
        });
        voidedVote.isVoid = function () {
            return true;
        };
        return voidedVote;
    } else {
        if (vVotesFound <= 1) { // voting for one photo makes no sense
            return new UCPVoteComment({
                poster: poster,
                node: challengeEntry,
                ucpThread: ucpThread
            });
        } else {
            var vote = new UCPVerticalVote({
                chlgname: challengeConfig.reName(), 
                poster: poster, 
                voteText: vVoteLines, 
                votesArray: vVotesArray, 
                node: challengeEntry,
                ucpThread: ucpThread
            });
            if (exampleVote) {
                vote.isExampleVote = function () {
                    return true;
                };
            }
            return vote;
        }
    }
}

function parseHorizontalVote(data) {
    var ucpThread = data.ucpThread;
    var picType = ucpThread.challengeDefinition().scoreType().match('PIC-H');
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var replytxt = data.replytxt;

    // first: cleanup the comment, to ease the vote recognition

    // in The mother of all challenge group, the 'local' CP script has no way to handle '10'; 
    // they use ten instead
    // people that use this hack, also use it in other groups:
    replytxt = replytxt.replace(/[\s,-]+ten[\s,-]+/g,  "-10-");
    replytxt = replytxt.replace(/[\s,-]+nine[\s,-]+/g,  "-9-");
    replytxt = replytxt.replace(/[\s,-]+eight[\s,-]+/g, "-8-");
    replytxt = replytxt.replace(/[\s,-]+seven[\s,-]+/g, "-7-");
    replytxt = replytxt.replace(/[\s,-]+six[\s,-]+/g,   "-6-");
    replytxt = replytxt.replace(/[\s,-]+five[\s,-]+/g,  "-5-");
    replytxt = replytxt.replace(/[\s,-]+four[\s,-]+/g,  "-4-");
    replytxt = replytxt.replace(/[\s,-]+three[\s,-]+/g, "-3-");
    replytxt = replytxt.replace(/[\s,-]+two[\s,-]+/g,   "-2-");
    replytxt = replytxt.replace(/[\s,-]+one[\s,-]+/g,   "-1-");
    replytxt = replytxt.replace(/[\s,-]+nil[\s,-]+/g,   "-0-");
    replytxt = replytxt.replace(/[\s,-]+zero[\s,-]+/g,  "-0-");
    replytxt = replytxt.replace(/[\s,-]+o[\s,-]+/g,     "-0-");
    var replyLines = replytxt.split(/<br>\s*/); // source code shows '<br />', DOM uses <br>?
    var horizontalVotes = null;
    $each(replyLines, function (replyLine) {
        if (replyLine.match(/void/i)) {
            // Skip lines where the vote has been voided
            return;
        }
        // check for comments before any other manipulations
        // corrections of the form 'corrected:', 'correction:' (need the ':' -> '1-2-3 with correction')
        replyLine = replyLine.replace(/cor(?:r)?ected:|correction:/, '&gt;&gt;'); // corrected sometimes mis-spelled
//      removed: vote after misvote '1-2-3 (with correction)' where seen as regular comments
        if (!replyLine || replyLine.replace(/<[^>]*>/g, '')          // remove html tags
                                       .replace(/^\s*/g, '')             // remove leading spaces
                                       .replace(/^foto[^\d]*|^photo[^\d]*/i, '')     // remove 'photo' in 'photo 2: 1-1-1'
                                       .replace(/(&gt;)+/, '') // don't ignore corrections (>> or -->)
                                       .match(/^[^\d]{6}/)) { // 6? arbitrary
            return;
        }
        replyLine = replyLine.replace(/<a[^<]*<\/a>/g, ''); // removes links
        replyLine = replyLine.replace(/\s*<[^>]*>\s*/g, ''); // removes html tags

        // remove any leading white space
        replyLine = replyLine.replace(/^\s*/g, '');
        replyLine = replyLine.replace(/^foto[^\d]*|^photo[^\d]*/i, '');
        if (!replyLine) {
            return;
        }
        replyLine = replyLine.replace(/^.*(?:&gt;|\||→)+\s*/g, ''); // correction
        // some like voting with extra spaces '0 - 1 - 2'
        // must be done after correction characters
        //replyLine = replyLine.replace(/(\d+)\s+[^a-zA-Z0-9]/g, "$1-");
        // remove the trailing '-'
        replyLine = replyLine.replace(/-$/, '');
        // ignore lines that are comment text
        if (!replyLine || replyLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            return;
        }

        // catches multiple votes, or comment, on multiple lines
        var matchVotes = ucpGetReHorizontalVoteMatch().exec(replyLine);
        while (matchVotes) { // catches multiple votes (correction?) on one line
            horizontalVotes = matchVotes;
            matchVotes = ucpGetReHorizontalVoteMatch().exec(replyLine);
        }
    });
    if (horizontalVotes) {
        var nvotes = horizontalVotes.length - 1;
        // vote: 1-2-3
        //    if (nvotes >= neededPhotos) {
        var hVotesArray = [];
        var voteIdx     = 1; // 0: the input string
        while (!horizontalVotes[voteIdx]) {
            ++voteIdx; // skip the non matching part
        }
        var arrayIdx = 1;
        while (true) {
            if (picType) {
                hVotesArray[parseInt(horizontalVotes[voteIdx], 10)] = 1;
            } else {
                hVotesArray[arrayIdx] = parseInt(horizontalVotes[voteIdx], 10);
            }
            if (voteIdx + 1 >= horizontalVotes.length) {
                break;
            }
            if (!horizontalVotes[voteIdx + 1]) {
                break;
            }
            ++voteIdx;
            ++arrayIdx;
        }
        var retval = picType ?
                        new UCPPICHorizontalVote({
                            chlgname: ucpThread.challengeName(), 
                            poster: poster, 
                            voteText: horizontalVotes, 
                            votesArray: hVotesArray,
                            node: challengeEntry,
                            ucpThread: ucpThread
                        }) :
                        new UCPHorizontalVote({
                            chlgname: ucpThread.challengeName(), 
                            poster: poster, 
                            voteText: horizontalVotes, 
                            votesArray: hVotesArray,
                            node: challengeEntry,
                            ucpThread: ucpThread
                        });
        if (exampleVote) {
            retval.isExampleVote = function () {
                return true;
            };
        }
        return retval;
    } else {
        return new UCPVoteComment({
            poster: poster,
            node: challengeEntry,
            ucpThread: ucpThread
        });
    }
}

function parseVerticalWeightedVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var replytxt = data.replytxt;
    var challengeConfig = ucpThread.challengeDefinition();

    var vwVotesArray = [];
    var vwVoteLines = replytxt.split(/<br>\s*/); // source code shows '<br />', DOM uses <br>?
    // voteLines is an array:
    // #01: 3+1 (4)
    // #02: 3 (3)
    // ..
    // others use 1:
    var reVwVoteMatch1 = /(\d{1,2})#/;          // 01: -> no rating on this photo
    var reVwVoteMatch2 = /(\d{1,2})#(\d+)/;     // '01:1' --> simple rating on this photo
    var reVwVoteMatch3 = /(\d{1,2})#\d(\d|\s|\+)+\d+/; // sometimes, someone forgets the sum: 01:1+2
    var reVwVoteMatch4 = /(\d{1,2})#(\d|\s|\+|\(\d+\))*\((\d+)\)/; // '01:1+2(3)' && '01:1+2+3(4)' && '01:(2)' && '01:1+2(3)+2(5)'
    var vwVotesFound = 0;
    var vwVoided = false;
    for (var j = 0, vwVoteLinesLength = vwVoteLines.length; j < vwVoteLinesLength; ++j) {
        var vwVoteLine = vwVoteLines[j];
        // remove any leading spaces
        vwVoteLine = vwVoteLine.replace(/^\s*/, '');
        // ignore lines that are comment text
        if (vwVoteLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            continue;
        }
        // first seperate the photo number from the votes, in case it was seperated with a space
        var reVwReplaceMatch = /#(\d{1,2})/;
        vwVoteLine = vwVoteLine.replace(reVwReplaceMatch, "$1"); // remove leading #
        reVwReplaceMatch = /^(\d{1,2})([\-:+ ]+|\.{2,3})/;
        vwVoteLine = vwVoteLine.replace(reVwReplaceMatch, "$1#"); // insert # after photonumber
        // remove spaces
        vwVoteLine = vwVoteLine.replace(/\s/g, '');
        // remove any character before first vote in voteLine 00#-1+2
        vwVoteLine = vwVoteLine.replace(/(\d{1,2}#)[^0-9]*/, "$1"); // a remaining '-' would make it a negative vote :)
        if (vwVoteLine.match(/^\s*\d{1,2}\s*$/)) {
            vwVoteLine = vwVoteLine.replace(/^s*(\d{1,2})\s*$/, "$1#0"); // add a 0 as vote, where no rating is given
        }
        if (vwVoteLine.match(/void/i) && !vwVoteLine.match(/^\d/)) {
            // Skip replies where the vote has been voided
            vwVoided = true;
            break;
        }
        var matchedString, photoNumber, photoScore;
        var verticalWVotes = reVwVoteMatch4.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            photoScore = verticalWVotes[3];
            vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++vwVotesFound;
            continue;
        }
        verticalWVotes = reVwVoteMatch3.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            reVwVoteMatch = /(\d{1,2})#([\d\s\+]+)/.exec(vwVoteLine);
            try {
                verticalWVotes = eval(reVwVoteMatch[2]);
                vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = verticalWVotes;
                ++vwVotesFound;
                continue;
            } catch (e) {
                // well, we tried, didn't we
            }
        }
        verticalWVotes = reVwVoteMatch2.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            photoScore = verticalWVotes[2];
            vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++vwVotesFound;
            continue;
        }
        verticalWVotes = reVwVoteMatch1.exec(vwVoteLine);
        if (verticalWVotes) {
            photoNumber = verticalWVotes[1];
            vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = 0;
            ++vwVotesFound;
            continue;
        }
        // thisvotes is still null => normal comment
    }       
    //GM_log(vwVotesArray);
    if (vwVoided) {
        var voidedWVote = new UCPVote({
            poster: poster, 
            voteText: vwVoteLines, 
            votesArray: vwVotesArray, 
            node: challengeEntry,
            ucpThread: ucpThread
        });
        voidedWVote.isVoid = function () {
            return true;
        };
        return voidedWVote;
    } else {
        if (vwVotesFound <= 1) { // voting for one photo makes no sense
            return new UCPVoteComment({
                           poster: poster,
                           node: challengeEntry,
                           ucpThread: ucpThread
                       });
        } else {
            var vote = new UCPVerticalWeightedVote({
                                chlgname: challengeConfig.reName(), 
                                poster: poster, 
                                voteText: vwVoteLines, 
                                votesArray: vwVotesArray, 
                                node: challengeEntry,
                                ucpThread: ucpThread
                            });
            if (exampleVote) {
                vote.isExampleVote = function () {
                    return true;
                };
            }
            return vote;
        }
    }
}

function parseRatePhotoVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var replytxt = data.replytxt;
    var challengeConfig = ucpThread.challengeDefinition();
    var rVotesArray = [];
    var rVoteLines = replytxt.split(/<br>\s*/); // source code shows '<br />', DOM uses <br>
    // voteLines is an array (olho no lance):
    // photo 01: 7 pontos
    // photo 02: 4 pontos
    // ..
    // or (20 temas fotograficos)
    // 3 puntos: #12
    // 2 puntos: #2
    // ..
    // or (NA Nature)
    // 1°-#8
    // 2°-#1
    // 3°-#5
    var reVoteMatch1 = /(?:photo|foto|#)?\s*(\d+)[^\d]*(\d+)\s*p/i;
    var reVoteMatch2 = /(\d+)\s*p[^\d]*(\d+)/i;
    var reVoteMatch3 = /^#?(\d+)/;
    var reVoteMatch4 = /(\d+)\s*[,-]\s*(\d+)[,-]\s*(\d+)/;
    var reVoteMatch5 = /^(?:1|2|3)[^\d]+(\d+)/; // NA Nature
    var rVotesFound = 0;
    var weight = 3; // the top one is worth 3p
    rVoteLines.forEach(function (rVoteLine) {
        // remove any leading spaces
        rVoteLine = rVoteLine.replace(/^\s*/, '');
        if (rVoteLine.match(/void/i)) {
            // Skip replies where the vote has been voided
            return;
        }
        var matchedString, photoNumber, photoScore;
        var rateVotes = reVoteMatch1.exec(rVoteLine);
        if (rateVotes) {
            photoNumber = rateVotes[1];
            photoScore = rateVotes[2];
            rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++rVotesFound;
            return;
        }
        rateVotes = reVoteMatch2.exec(rVoteLine);
        if (rateVotes) {
            photoScore = rateVotes[1];
            photoNumber = rateVotes[2];
            rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(photoScore, 10);
            ++rVotesFound;
            return;
        }
        rateVotes = reVoteMatch4.exec(rVoteLine);
        if (rateVotes) {
            rVotesArray[parseInt(rateVotes[1].replace(/^0/, ''), 10)] = challengeConfig.equalWeights() ? 2 : 3;
            rVotesArray[parseInt(rateVotes[2].replace(/^0/, ''), 10)] = 2;
            rVotesArray[parseInt(rateVotes[3].replace(/^0/, ''), 10)] = challengeConfig.equalWeights() ? 2 : 1;
            ++rVotesFound;
            return;
        }
	rateVotes = reVoteMatch5.exec(rVoteLine);
        if (rateVotes) {
            photoNumber = rateVotes[1];
            rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = weight;
            weight--;
            ++rVotesFound;
            return;
        }
        rateVotes = reVoteMatch3.exec(rVoteLine); // as last: matches above two
        if (rateVotes) {
            photoNumber = rateVotes[1];
            rVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = weight;
            ++rVotesFound;
            weight--;
            return;
        }
        // thisvotes is still null => normal comment
    });
    //my_log(vVotesArray);
    if (rVotesFound <= 0) {
        return new UCPVoteComment({
                    poster: poster, 
                    node: challengeEntry,
                    ucpThread: ucpThread
                });
    } else {
        var vote = new UCPRatePhotoVote({
                    chlgname: challengeConfig.chlgname, 
                    poster: poster, 
                    voteText: rVoteLines, 
                    votesArray: rVotesArray, 
                    node: challengeEntry,
                    ucpThread: ucpThread
                });
        if (exampleVote) {
            vote.isExampleVote = function () {
                return true;
            };
        }
        return vote;
    }
}

function parsePICVerticalVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var replytxt = data.replytxt;
    var challengeConfig = ucpThread.challengeDefinition();
    var picX = data.picX;

    // scores are a single number, on separate lines
    var pvVotesArray = [];
    var pvVoteLines = replytxt.split(/<br>\s*/); // source code shows '<br />', DOM uses <br>?
    var pVotesFound = 0;
    var possibleExampleVote = true; 
    // BUG: http://www.flickr.com/groups/the-storybook-challenge-group/discuss/72157625609019459/
    // 1. pic-v-3 with vote 2 3 1, is not an example vote
    // 2. the second vote is considered an example vote; should never happen
    pvVoteLines.each(function (pvVoteLine, k) {
        var pvVoteLine = pvVoteLines[k];
        // remove any leading spaces
        pvVoteLine = pvVoteLine.replace(/^\s*/, '').replace(/#\s+(\d+)/, "$1");
        pvVoteLine = pvVoteLine.replace(/^foto[^\d]*|^photo[^\d]*/i, '');
        pvVoteLine = pvVoteLine.replace(/^n°[^\d]*/i, '');
        // ignore lines that are comment text
        if (pvVoteLine.match(/^\s*[a-zA-Z\s\.\,\(]{6}/)) { // 6? arbitrary
            return;
        }
        var reReplaceMatch = /#(\d{1,2})/;
        pvVoteLine = pvVoteLine.replace(reReplaceMatch, "$1"); // remove leading #
        var picVoteMatch = /^\s*(\d+)/.exec(pvVoteLine);
        if (picVoteMatch) {
            var voteIdx = parseInt(picVoteMatch[1].replace(/^0(\d+)/, '$1'), 10);
            if (voteIdx < 256) { // if someone enters a 'summary' of the scores (2302042141) => oom
                pvVotesArray[voteIdx] = 1;
                ++pVotesFound;
                return;
            }
        }
    });
    if (picX === undefined || pVotesFound <= 0) {
        return new UCPVoteComment({
                       poster: poster, 
                       node: challengeEntry,
                       ucpThread: ucpThread
                   });
    } else { // picX defined
        if (pVotesFound < picX) {
            return new UCPVoteComment({
                       poster: poster,
                       node: challengeEntry,
                       ucpThread: ucpThread
                   });
        } else {
            var picVVote = new UCPPICVerticalVote({
                       chlgname: challengeConfig.reName(), 
                       poster: poster, 
                       voteText: pvVoteLines, 
                       votesArray: pvVotesArray, 
                       node: challengeEntry,
                       ucpThread: ucpThread
                   });
            // also catches rules:
            // 1) ...
            // 2) ...
            // so, try to remove these
            /*
            if (!exampleVote) {
                var picExampleVote = picX > 1; // assume an example vote, only if voting for multiple
                                              // voting for photo 1 results in example vote otherwise
                GM_log("checking for example vote: ");
                pvVotesArray.each(function (vote, idx) {
                // TODO: check
                GM_log("idx: " + idx + " - vote: " + vote);
                    if (idx == 0) return; // skip index 0: 1-based voting
                    if (picExampleVote && (!vote || isNaN(vote))) {
                        picExampleVote = false;
                        return;
                    }
                });
            }*/
            if (/*picExampleVote ||*/ exampleVote) {
                picVVote.isExampleVote = function () {
                    return true;
                }; // override default behaviour
            }
            return picVVote;
        }
    }
}

function parsePICPlayerVote(data) {
    var ucpThread = data.ucpThread;
    var exampleVote = data.exampleVote;
    var poster = data.poster;
    var challengeEntry = data.challengeEntry;
    var replytxt = data.replytxt;
    var photoArray = data.photoArray;
    var challengeConfig = ucpThread.challengeDefinition();

    // used in MatchPoint (http://www.flickr.com/groups/matchpoint/discuss/)
    var ppVoteLines = replytxt.split(/<br>\s*/); // source code shows '<br />', DOM uses <br>?
    var score, player;
    var ppVote = undefined;
    ppVoteLines.each(function (ppVoteLine, l) {
	// TODO: add new variable to challengeDefinition: voteWithBuddyIcon
        // (remove buddyicons first: contains @)
        ppVoteLine = ppVoteLine.replace(/<img[^>]+>/g, '');
        // ignore lines that are comment text
        if (!ppVoteLine.match(/@|#|p(oi)?nt/i)) {
            return;
        }
        // remove any leading spaces
        ppVoteLine = ppVoteLine.replace(/^\s*/, '');
        // remove html, which does not fit in a username
        ppVoteLine = ppVoteLine.replace(/[@#](\s*[^<]*)/, '@ $1');
        var picPVoteMatch = /(\d+)[^\d]*[@#]\s*([^<]*)/.exec(ppVoteLine);
        if (!picPVoteMatch) {
            picPVoteMatch = ppVoteLine.match(/(?:point|pnt)[^\d]*(\d+)[^@#]*[@#]\s*([^<]*)/i);
        }
        var ppVotesArray = []; //[challengeConfig.options.neededPhotos + 1];
        if (picPVoteMatch) {
            score  = picPVoteMatch[1];
            player = picPVoteMatch[2];
            // cleanup player
            player = player.replace(/\)/g, ''); // smiley's !!
            player = player.replace(/<[^>]*>/g, '').replace(/\s*$/, '');
            player = player.replace(/&nbsp;/g, ' ');
            // remove insignificant characters at the end of the player entry
            player = player.replace(/(?:\.|\!|\s)+$/, '');
            // compensate for spaces, and underscores in usernames, or the lack of them
            player = player.replace(/(?:_|\s)+/g, '');
            // people tend to mistype usernames with numbers
            if (player.replace(/\d/g, '').length > 0 ) {
                player = player.replace(/\d/g, '');
            }
            photoArray.each(function (photo, ppPhotoIdx) {
                var photo = photoArray[ppPhotoIdx];
                if (!(photo instanceof UCPCompetitor)) {
                    return;
                }
                var challenger = photo.poster().username.replace(/(?:_|\s|\))+/g, '');
                if (challenger.replace(/\d/g, '').length > 0) {
                    challenger = challenger.replace(/\d/g, '');
                }
                // voters make shortcuts for usernames, and don't care for case
                // they also misspell Mustela, and anglerove
                if (photo && (
                        (challenger.match(/Mustela.Nivalis/) && player.match(/Must(e|a)l?a/i)) ||
                        (challenger.match(/anglerove/) && player.match(/angelrove/i))
                    )) {
                    ppVotesArray[ppPhotoIdx + 1] = 1; //parseInt(score, 10);
                        ppVote = new UCPPICPlayerVote({
                                    chlgname: challengeConfig.reName(), 
                                    poster: poster,
                                    voteText: ppVoteLines, 
                                    votesArray: ppVotesArray, 
                                    node: challengeEntry,
                                    ucpThread: ucpThread
                                });
                    return;
                }
        try {
                if (photo && challenger.toLowerCase().match(player.toLowerCase())) {
                    ppVotesArray[ppPhotoIdx + 1] = 1; // parseInt(score, 10);
                        ppVote = new UCPPICPlayerVote({
                                    chlgname: challengeConfig.reName(), 
                                    poster: poster, 
                                    voteText: ppVoteLines, 
                                    votesArray: ppVotesArray, 
                                    node: challengeEntry,
                                    ucpThread: ucpThread
                                });
                }
} catch (e) {
    GM_log("error: " + e);
        GM_log("challenger='" + challenger + "' - player='" + player + "'");
}
            });
        }
    });
    if (!ppVote) {
        ppVote = new UCPVoteComment({
                        poster: poster,
                        node: challengeEntry,
                        ucpThread: ucpThread
                    });
    }
    return ppVote;
}

// The challenger or competitor's photo in  one of the challenges.
var UCPCompetitor = new Class({
    Implements: [Options],
    options: {
        node: null,
        photo: null,
        poster: null,
        owner: null,
        photoId: null,
	photoTitle: null,
	photoNumber: null,
        comments: [],
	approved: false,
	approvedBy: null,
        ucpThread: null
    },
    initialize: function (options) {
        this.setOptions(options);
    },
    ucpThread: function () {
        return this.options.ucpThread;
    },
    toString: function () {
        return this.options.poster.username;
    },
    // accessors
    node: function () {
        return this.options.node;
    },
    photo: function () {
        return this.options.photo;
    },
    poster: function () {
        return this.options.poster;
    },
    owner: function () {
        return this.options.owner;
    },
    photoId: function () {
        return this.options.photoId;
    },
    photoTitle: function () {
    	return this.options.photoTitle;
    },
    photoNumber: function () {
    	return this.options.photoNumber;
    },
    approved: function () {
    	return this.options.approved;
    },
    approvedBy: function () {
    	return this.options.approvedBy;
    },
    hasError: function () {
        return this.options.comments.some( function (msg) {
            return msg.type == 'error';
        });
    },
    hasWarning: function () {
        return this.options.comments.some( function (msg) {
            return msg.type == 'warning';
        });
    },
    error: function () {
	var retval = '';
        this.options.comments.each(function (comment) {
            if (comment.type === 'error') {
                if (retval.length > 0) retval += " - ";
		retval += comment.msg;
            }
        });
        return retval;
    },
    warning: function () {
	var retval = '';
        this.options.comments.each(function (comment) {
            if (comment.type === 'warning') {
                if (retval.length > 0) retval += " - ";
		retval += comment.msg;
            }
        });
        return retval;
    },
    printStatus: function (ucpReplyDecoratorFactory) {
        if (!$chk(this.replyDecorator)) {
            this.replyDecorator = ucpReplyDecoratorFactory.createPhotoDecorator();
        }
        this.replyDecorator.decorateReply(this);
    },
    addError: function (error) {
        this.options.comments.include({ msg: error, type: 'error' });
    },
    addWarning: function (warning) {
        this.options.comments.include({ msg: warning, type: 'warning' });
    },
    addComment: function (comment) {
        this.options.comments.include({ msg: comment, type: 'comment' });
    },
    checkForValidPoster: function () {
        if ($chk(this.owner()) && (this.poster().userid !== this.owner().userid)) {
            // get the real photo owner's id
            var apiData = {
                api_key: GM_getMagisterLudi(),
                auth_hash: GM_getAuthHash(),
                auth_token: GM_getAuthToken(),
                format: 'json',
                method: 'flickr.photos.getInfo',
                nojsoncallback: 1,
                photo_id: this.photoId()
            };
            var realOwnerId, realUsername;
            new Request({
                url: "http://www.flickr.com/",
                async: false,
                onSuccess: function (responseText, responseXML) {
                    var result;
                    try {
                        result = JSON.parse(responseText);
                    } catch (e) {
                        result = eval('(' + responseText + ')');
                    }
                    if (result.stat === 'fail') {
                        return;
                    }
                    realOwnerId = result.photo.owner.nsid;
                    realUsername = result.photo.owner.username;
                }
            }).get("/services/rest/", apiData);
            this.owner().userid = realOwnerId;
            this.owner().username = realUsername; // Alesa Dam
            var buddyicon = this.photo().getParent('td.Said').getParent('tr').getElement('td.Who a img');
            var realPosterId = /static\.?flickr.com\/\d+\/buddyicons\/(\d+@\w\d+)\.jpg/.exec(buddyicon.get('src'))
            if ($chk(realPosterId)) {
                this.poster().userid = realPosterId[1];
            } else { // if not, default grey smiley
	    	realPosterId = /buddyicon.jpg#(\d+@\w\d+)$/.exec(buddyicon.get('src'));
		if ($chk(realPosterId)) {
			this.poster().userid = realPosterId[1];
		}
	   }
            return this.poster().userid === this.owner().userid;
        }
        return true;
    }
});

var UCPVoteComment = new Class({
    Implements: [Options],
    options: {
        ucpThread: null,
        poster: null,
        comments: [],
        node: null
    },
    initialize: function (options) {
        this.setOptions(options);
    },
    toString: function () {
        return this.options.poster.username;
    },
    poster: function () {
        return this.options.poster;
    },
    node: function() {
        return this.options.node;
    },
    ucpThread: function () {
        return this.options.ucpThread;
    },
    printStatus: function (ucpReplyDecoratorFactory) {
        if (!$chk(this.replyDecorator)) {
            this.replyDecorator = ucpReplyDecoratorFactory.createCommentDecorator();
        }
        this.replyDecorator.decorateReply(this);
    }
});

function ucpCreateCompetitor(data) {
	var photoNode = data.node;
	var poster = data.poster;
	var photoNumber = data.number;
	var ucpThread = data.ucpThread;

    var ownerId;
    var photoId = photoNode.get('src').match(/https?:\/\/.*flickr.com\/\d+\/(\d+)_.*/)[1];
    var photoTitle = $chk(photoNode.get('alt')) ? photoNode.get('alt') : photoNode.get('title');
    if (photoNode.getParent('a') && photoNode.getParent('a').get('href')) {
        ownerId = photoNode.getParent('a').get('href').split('/')[4];
        //GM_log("ownerId: " + ownerId);
        var owner;
        if (ownerId === poster.userid) {
            owner = poster;
        } else {
            owner = { username: "", userid: ownerId };
        }
        var textNode = photoNode.getParent('td.Said').getElement('p');
        var approvalImg = textNode.getElement('img[alt*="UCPAapproved"]');
        if (approvalImg && ucpThread.topic()) {
            var approvalCheck = ucpCheckPhotoApproval(photoNode, ucpThread.topic());
            // returns
            // {
            //      approved: true/false,
            //      approver: string,
            //      version: number,
            //      checksum: true/false,
            //      photoChecksum: true/false,
            //      error: string
            //      photoId: string
            //  }
            var retval;
            var photoText;
            var comment;
            if (approvalCheck.approved) {
                comment = { msg: " approved by " + approvalCheck.approver, type: 'comment' };
                return new UCPCompetitor({
                    node: photoNode.getParent('td.Said').getElement('p'),
                    photo: photoNode,
                    photoId: photoId,
		    photoTitle: photoTitle,
		    photoNumber: photoNumber,
                    comments: [ comment ],
                    poster: poster,
                    owner: owner,
		    approved: true,
		    approvedBy: approvalCheck.approver,
                    ucpThread: ucpThread
                });
            } else if (approvalCheck.ignored) {
                comment = { msg: "non competing image (by " + approvalCheck.approver + ")", type: 'comment' };
                return new UCPVoteComment({
                    node: photoNode.getParent('td.Said').getElement('p'),
                    poster: poster,
                    comments: [ comment ],
                    ucpThread: ucpThread
                });
            } else {
                retval = new UCPCompetitor({
                    node: photoNode.getParent('td.Said').getElement('p'),
                    photo: photoNode,
                    photoId: photoId,
		    photoTitle: photoTitle,
		    photoNumber: photoNumber,
                    poster: poster,
                    owner: owner,
		    approved: true,
		    approvedBy: approvalCheck.approver,
                    ucpThread: ucpThread
                });
		// if (adminOrMod) {
                	retval.addError(approvalCheck.error);

                	return retval;
		//}
            }
        } else {
            return new UCPCompetitor({
                node: photoNode.getParent('td.Said').getElement('p'),
                photo: photoNode,
                photoId: photoId,
		photoTitle: photoTitle,
		photoNumber: photoNumber,
                poster: poster,
                owner: owner,
                ucpThread: ucpThread
            });
        }
    } else {
        comment = { msg: "photo has no link to photo page", type: 'error' };
        return new UCPCompetitor({
            node: photoNode.getParent('td.Said').getElement('p'),
            photo: photoNode,
            photoId: photoId,
	    photoTitle: photoTitle,
	    photoNumber: photoNumber,
            poster: poster,
            comments: [ comment ],
            ucpThread: ucpThread
        });





    }
}

    function ucpFindPhotos(node, ucpThread, all, poster) {
        var photoArray = [];
        if (ucpThread.challengeDefinition().scoreType() === "MEETANDGREET" && !all) { // don't bother
            return photoArray;
        }
        if (!$chk(poster)) {
            poster = ucpGetUsername(node);
        }
        if (!$chk(poster)) {
            return photoArray;
        }

        // there are posters that don't add the 'a' anchor
        // needs a 'p//a': some scripts add <b> around image
        //var photos = node.getElement('p').getElements("(a img, img)(not[src*=buddyicons] and [src*=static.flickr.com] and [src$=jpg] and (not[height] or [height > 100]) and (not[width] or [width > 100]))");//, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        var photos = node.getElements("img").filter(ucpCheckPhoto, ucpThread.challengeDefinition());
        var potentialCompetitor;
        photos.each(function(photoNode) {
            if (potentialCompetitor !== undefined && !all) {
                return;
            }
	    var photoNumber = undefined;
	    if (groupPreferences.checkPhotoNumbering()) {
                    photoNode.getParent('p').get('text').split('\n').some( function (line) {
                        try {
                                photoNumber = line.trim().replace(/^(&nbsp;|&emsp;)+/, '').match(/^#?\s*(\d+)/)[1];
                        } catch (e) {
                                // ignore: most lines won't match
                        }
                });
            }
            potentialCompetitor = ucpCreateCompetitor({
		node: photoNode,
		poster: poster,
		number: photoNumber, 
		ucpThread: ucpThread
	    });
            if (potentialCompetitor) {
                photoArray.push(potentialCompetitor);
            }
        });
        return photoArray;
    }

    function ucpGetUsername(node) {
        var usernameNode;
        try {
            usernameNode = node.getElement('h4 a[href*=photos]');
        } catch (e) {
            if (!usernameNode) {
                try {
                    usernameNode = node.getParent('td.Said').getElement('h4 a[href*=photos]');
                } catch (e) {
                    GM_log("error fetching user name: " + e);
                }
            }
        }

        if (usernameNode) {
            var username = usernameNode.textContent.replace(/&amp;/g, "&"); //.replace(/<[^>]*>/g, ''); user '<Jamie>'
            if (username.length === 0) {
                GM_log("empty user in element of type " + node); // should not happen!
                return null;
            }
            var userid = usernameNode.href.split('/')[4];
            // don't put username in contains part: trouble with special chars: ' " ( ) ...
            var admin  = node.getElement('h4 a img[src*=icon_member_admin]');
            if (!admin) {
                admin = node.getElement('h4 a img[src*=icon_moderator]');
            }
            return { username: username, userid: userid, admin: admin ? true : false };
        } else {
        // deleted users go into a h4, without href
            try {
                usernameNode = node.getElement('h4');
            } catch (e) {
                if (!usernameNode) {
                    try {
                        usernameNode = node.getParent('td.Said').getElement('h4');
                    } catch (e) {
                        GM_log("error fetching user name: " + e);
                    }
                }
            }
        
            if (!usernameNode) {
                GM_log("no user");
                return null;
            }

            var username = usernameNode.textContent.replace(/&amp;/g, "&");
            if (username.length === 0) {
                GM_log("empty user");
                return null;
            }
            return { username: username, userid: null, admin: false };
        }
    }


    function ucpCollectVotes(ucpThread, challengeAnnouncement, challengeEntries) {

        var groupConfig = ucpThread.groupConfig();
        var challengeConfig = ucpThread.challengeDefinition();
        var topic = ucpThread.topic();
        var voteArray = [];
        var commentArray = [];
        var photoArray = [];
   
        if (challengeAnnouncement) {
            photoArray.combine(ucpFindPhotos(challengeAnnouncement, ucpThread, false));
            /*.each(function (photo) {
                if ($chk(photo)) {
                    if (photo instanceof UCPCompetitor) {
                        photoArray.include(photo);
                    }
                }
            });*/
        }
        challengeEntries.each(function (challengeEntry, i) {
            if (i === 0) { // announcement
                return;
            }
            if (i === 1 && groupConfig.skipFirstReply()) { 
                return;
            }
            //GM_log("["+i+"]: "+challengeEntry.innerHTML);
            // admins have an image before there name, surrounded with an 'a' tag without 'href' attribute :> skip
            var poster = ucpGetUsername(challengeEntry);
            if (!poster) {
                GM_log("no poster found");
                return;
            }

            var usercomment = challengeEntry.getElement('p');

            if (!$chk(usercomment)) {
                GM_log("no usercomment in " + challengeConfig.reName());
                return;
            }

            var photosInNode = ucpFindPhotos(challengeEntry, ucpThread, false, poster);
            if (photosInNode.length > 0) {
                photoArray.combine(photosInNode);
            } else {
                // This should be a vote or user comment
                if (i === 2 && groupConfig.skipFirstTwoRepliesForVotes()) {
                    return;
                }
                var usercommentClone = $(usercomment.cloneNode(true));
                // only consider votes that are after the last photo (only approximate)
                // if the next comment has a photo, skip!
                if (voteArray.length === 0) {
                    var nextComment = challengeEntries[i + 1];
                    if (nextComment && ucpFindPhotos(nextComment, ucpThread, false).length > 0) {
                        commentArray.include(new UCPVoteComment({
                            node: challengeEntry,
                            poster: poster,
                            ucpThread: ucpThread
                        }));
                        return;
                    }
                }
                // first remove striked votes, image icons, banners
                var strikesAndStuff = usercommentClone.getElements("s, del, strike, img, a");
                strikesAndStuff.each(function (strike) {
                    strike.dispose();
                });
                var replytxt = usercommentClone.innerHTML.split("<small>")[0];
        
                var exampleVote = false;
                if (replytxt.match(/sample vote|example|ejemplo/i) && !challengeConfig.scoreType().match("PIC-P-")) {
                    // in PIC-P-n commenting is encouraged, often leading to "nice example of", or similar
                    exampleVote = true;
                }

                var votes = null;
                var ignore;
                switch (challengeConfig.scoreType()) {
                case "VERTICAL":
                    var vote = parseVerticalVote({
                                   replytxt: replytxt, 
                                   challengeEntry: challengeEntry,
                                   ucpThread: ucpThread,
                                   poster: poster, 
                                   exampleVote: exampleVote
                         });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;
                
                case "VERTICAL-WEIGHTED":
                    var vote = parseVerticalWeightedVote({
                                    replytxt: replytxt, 
                                    challengeEntry: challengeEntry,
                                    ucpThread: ucpThread,
                                    poster: poster,
                                    exampleVote: exampleVote
                                });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;

                case "RATE-PHOTO":
                    var vote = parseRatePhotoVote({
                                    replytxt: replytxt, 
                                    challengeEntry: challengeEntry,
                                    ucpThread: ucpThread,
                                    poster: poster,
                                    exampleVote: exampleVote
                                });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;
               
                case "HORIZONTAL":
                    var vote = parseHorizontalVote({
                                    replytxt: replytxt, 
                                    challengeEntry: challengeEntry,
                                    ucpThread: ucpThread,
                                    poster: poster,
                                    exampleVote: exampleVote
                                });
                    if (vote) {
                        if (vote instanceof UCPVoteComment) {
                            commentArray.include(vote);
                        } else {
                            voteArray.include(vote);
                        }
                    }
                    break;

                case "MEETANDGREET":
                    commentArray.include(new UCPVoteComment({
                        poster: poster, 
                        node: challengeEntry,
                        ucpThread: ucpThread
                    }));
                    break;

                default:
                //case "PIC-*":
                    if (/PIC-/.test(challengeConfig.scoreType())) {
                        // PIC-V-n: pic n photos, score vertically
                        // PIC-H-n: pic n photos, score horizontally
                        // PIC-P-1: give a point to the player
                        var picXmatch = /PIC-([HVP])-(\d+)/.exec(challengeConfig.scoreType());
                        var picX = undefined;
                        var picOrientation = undefined;
                        if (picXmatch && picXmatch.length > 2) {
                            picX = parseInt(picXmatch[2], 10);
                        }
                        if (picXmatch && picXmatch.length > 1) {
                            picOrientation = picXmatch[1];
                        }
                        if (picOrientation === 'H') {
                            var createdPicHVote = parseHorizontalVote({
                                                    exampleVote: exampleVote,
                                                    ucpThread: ucpThread, 
                                                    poster: poster, 
                                                    challengeEntry: challengeEntry, 
                                                    replytxt: replytxt
                                                });
                            if (createdPicHVote instanceof UCPVoteComment) {
                                commentArray.include(createdPicHVote);
                            } else {
                                voteArray.include(createdPicHVote);
                            }
                        } else if (picOrientation === 'V') {
                            // PIC-V
                            var createdPicVVote = parsePICVerticalVote({ 
                                                    exampleVote: exampleVote,
                                                    ucpThread: ucpThread, 
                                                    poster: poster, 
                                                    challengeEntry: challengeEntry, 
                                                    replytxt: replytxt,
                                                    picX: picX
                                                });
                            if (createdPicVVote instanceof UCPVoteComment) {
                                commentArray.include(createdPicVVote);
                            } else {
                                voteArray.include(createdPicVVote);
                            }
                        } else if (picOrientation === 'P') {
                            var createdPicPVote = parsePICPlayerVote({
                                                    exampleVote: exampleVote,
                                                    ucpThread: ucpThread,
                                                    poster: poster,
                                                    challengeEntry: challengeEntry,
                                                    replytxt: replytxt,
                                                    photoArray: photoArray
                                                });
                            if (createdPicPVote instanceof UCPVoteComment) {
                                commentArray.include(createdPicPVote);
                            } else {
                                voteArray.include(createdPicPVote);
                            }
                        }
                    }
                }
            }
        }); // each entry

        ucpThread.votesArray = voteArray;
	ucpThread.commentsArray = commentArray;
	ucpThread.photosArray = photoArray;
    }

    function ucpProcessVotes(ucpThread) {
	var photos = ucpThread.photos();
	var votes = ucpThread.votes();
        var doubleVotesHash = new Hash();

        var cummulativeScore = new UCPVote({
            chlgname: null, 
            poster: "flickr", 
            voteText: "none", 
            votesArray: [], 
            commentAnchor: null,
            ucpThread: ucpThread
        });
        var previousVote = undefined;
        $each(votes, function (vote, j) {
            if (vote.isVoid()) {
                vote.addMessage("found a voided vote");
                vote.printStatus(ucpThread.options.replyDecoratorFactory);
                return;
            }
            var theFirstAndExampleVote = previousVote === undefined && vote.isExampleVote();
            if (theFirstAndExampleVote) {
                vote.addMessage("found an example vote");
                vote.printStatus(ucpThread.options.replyDecoratorFactory);
            } else {
                vote.addMessage(
                        "<b>" + 

                            vote.poster().username


                        + 
                        (
                            vote.poster().admin ?
                            " (admin/mod)" : ""
                        ) +
                        "</b> voted");
                if (doubleVotesHash.has(vote.poster().userid)) { // this member already voted!
                    vote.addError("voted more than once");
                    //ucpThread.addVotingError("'" + vote.poster().username + "' voted more than once");
                } else {
                    doubleVotesHash.set(vote.poster().userid, vote.poster().username); // whatever
                }



                if (ucpThread.challengeDefinition().scoreType().match(/PIC-[PHV]-/) ||
                    ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO/)) {
                        
                    cummulativeScore.add(vote);
                } else { // HORIZONTAL, VERTICAL, VERTICAL-WEIGHTED
                    cummulativeScore = vote; // last vote contains the full score
                }
                if (ucpThread.finished(cummulativeScore)) {
                    ucpThread.updateStatus("finished");
                }
            }
            var currentValidVoting = vote.valid(previousVote, photos.length, j); // use the previousVote, even if it is an example
            if (!theFirstAndExampleVote) {
                var votedForString = vote.votedFor();
                if (votedForString) {
                    vote.addMessage("for photo" + (votedForString.match(/,/) ? "s " : " ") + votedForString);
                }
                if (!currentValidVoting && !ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
                    ucpThread.addVotingError("'" + vote.poster().username + "' " + vote.error());
                    if (!ucpThread.validVoting()) { // already in error from previous vote
                        vote.messages().each(function (message) {
                            message.type = 'warning';
                        });
                    }
                }
                // any PIC type should not show the intermediate result
                if (!ucpThread.challengeDefinition().scoreType().match(/PIC-[HV]/) &&
                    !ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO/)) {
                    var votingResult = ucpThread.challengeDefinition().scoreType().match(/PIC-P/) ? 
                        cummulativeScore.showPicResult() : cummulativeScore.showVotes();
                    vote.addMessage(votingResult);
                }
                if (ucpThread.challengeDefinition().playerVoting() === "maynotvote") {
                    if (photos.some(function (photo) { return vote.poster().userid == photo.poster().userid; })) {
                        ucpThread.addVotingError(
                             "'" + vote.poster().username + "' voted in a challenge he/she plays in");
                        vote.addError("in a challenge he/she plays in");
                    }
                }
                vote.printStatus(ucpThread.options.replyDecoratorFactory);
            }
            if (ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
                previousVote = cummulativeScore;
                previousVote.options.poster = vote.options.poster;
            } else {
                previousVote = vote;
            }
        });
        previousVote = undefined;

        //overwrite some base statusses if challenge is in voting.
        if (ucpThread.challengeName().match(groupConfig.states().closed)) { // should never happen
            ucpThread.updateStatus("closed");
        }
        if (ucpThread.finished(cummulativeScore)) {
            ucpThread.updateStatus("finished");
        }
        ucpThread.checkStatus();
        
        //if (ucpThread.challengeStatus() === "Finished"    || ucpThread.challengeStatus() === "Closed" ) {


            if (ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
                ucpThread.setScoreSummary(cummulativeScore.showPicResult(true));
            } else {
                ucpThread.setScoreSummary(cummulativeScore.showVotes(true));
            }
        //}
        //let's go and change the update status on screen
	ucpThread.setCummulativeScore(cummulativeScore)

	// if at least one entry of at least three entries is numbered, check the numbering
	if (groupPreferences.checkPhotoNumbering() && photos.length >= 3) {
		if (photos.some(function (competitor) {
					return $chk(competitor.photoNumber());
				})) {
			photos.each(function (competitor, idx) {
			    try {
				if (!$chk(competitor.photoNumber())) {
					competitor.addWarning("did not number his/her entry");
					ucpThread.addPhotoError("did not number his/her entry");
				} else if (competitor.photoNumber() != (idx + 1) ) {
					competitor.addWarning("numbered incorrectly");
					ucpThread.addPhotoError("numbered incorrectly");
				}
			    } catch (e) {
			    	alert("error: " + e);
			    }
			});
		}
        }
    }

// end former common library

function updateClock() {
        var nextPanda = GM_getValue(UCPprefix + ".clock.nextPanda");
        if (!$chk(nextPanda)) {
                getPandas(updateClock);
                return;
        } else {
                readFlickrClock();
                getPandas();
        }
}

function getPandas(callback) {
            new Request({
                url: 'http://www.flickr.com',
                onSuccess: function (responseText, responseXml) {
                        var result = JSON.parse(responseText);
                        if (result.stat != 'ok') {
                                GM_log("ERROR reading pandas " + responseText);
                        } else {
                                var pandas = result.pandas.panda.sort(function (a,b) {
                                        return (parseInt(Math.random() * 10) %2);
                                });
                                GM_setValue(UCPprefix + ".clock.nextPanda", pandas[0]._content);
                                if (callback) {
                                        callback();
                                }
                        }
                },
                onFailure: function (response) {
                        GM_log("ERROR reading pandas: " + response.statusText);
                }
            }).get('/services/rest', {
                api_key: GM_getMagisterLudi(),
                auth_hash: GM_getAuthHash(),
                auth_token: GM_getAuthToken(),
                format: 'json',
                nojsoncallback: 1,
                method: 'flickr.panda.getList'
            });
}

function readFlickrClock() {
        var lastupdateValue = GM_getValue(UCPprefix + ".clock.lastupdate");
        var intervalValue = GM_getValue(UCPprefix + ".clock.interval");
        var lastchecked = GM_getValue(UCPprefix + ".clock.lastcheck");
        var now = parseInt(new Date().getTime() / 1000); // API returns time in seconds
        if ($chk(lastchecked) && (parseInt(lastchecked) + parseInt(intervalValue) > now)) { // not waited long enough (API restriction)
                return;
        }
        if ($chk(lastchecked) && (now - parseInt(lastchecked) < 2 * 60 * 60)) { // only refresh every 2 hours
                return;
        }
        new Request({
                url: 'http://www.flickr.com',
                onSuccess: function (responseText, responseXml) {
                        var result = JSON.parse(responseText);
                        if (result.stat != 'ok') {
                                GM_log("ERROR reading clock: " + responseText);
                        } else {
                                var lastupdate = result.photos.lastupdate;
                                var interval = result.photos.interval;
                                var stored = GM_getValue(UCPprefix + ".clock.lastupdate");
                                if (!$chk(stored) || (parseInt(stored) !== parseInt(lastupdate))) {
                                        GM_setValue(UCPprefix + ".clock.lastupdate", lastupdate);
                                        GM_setValue(UCPprefix + ".clock.interval", interval);
                                        GM_setValue(UCPprefix + ".clock.lastcheck", now);
                                }
                        }
                },
                onFailure: function (response) {
                        GM_log("ERROR reading clock: " + response.statusText);
                }
        }).get('/services/rest', {
                api_key: GM_getMagisterLudi(),
                auth_hash: GM_getAuthHash(),
                auth_token: GM_getAuthToken(),
                format: 'json',
                nojsoncallback: 1,
                method: 'flickr.panda.getPhotos',
                panda_name: GM_getValue(UCPprefix + ".clock.nextPanda"),
                extras: 'date_upload',
                per_page: 1
        });
}

function calculateFlickrClock() {
        var flickrClockSeconds = GM_getValue(UCPprefix + ".clock.lastupdate");
        if ($chk(flickrClockSeconds)) {
                var oldFlickrClock = new Date(parseInt(flickrClockSeconds) * 1000);
                var flickrClockLastCheck = new Date(parseInt(GM_getValue(UCPprefix + ".clock.lastcheck")) * 1000);
		if (flickrClockLastCheck.toString().match(/invalid/i)) {
			GM_deleteValue(UCPprefix + ".clock.lastcheck");
			GM_deleteValue(UCPprefix + ".clock.lastupdate");
			return undefined;
		}
                var now = new Date();
                var timeElapsed = now - flickrClockLastCheck;
                var estimatedFlickrClock = new Date(oldFlickrClock.getTime() + timeElapsed);
                //GM_log("DEBUG: storedClock='" + oldFlickrClock + "' - lastCheck='" + flickrClockLastCheck + "' - elapsed='" + timeElapsed + "' - estimated='" + estimatedFlickrClock + "'");
                return estimatedFlickrClock;
        }
        return undefined;
}

// defaults
var username = GM_getLoggedInUser();
var user;
var userNsid;
var groupConfig;
var groupPreferences;
var ucpGroupConfigReader = new UCPGroupConfigReader(); 
var topicListingTable;
var challengeGroup = false;

function initialize() {
    try {
        groupConfig = ucpGroupConfigReader.createGroupConfig();
    } catch (e) {
        GM_log("non-challenge group; tweaking for regular group");
    }

    if ($chk(groupConfig)) {
        challengeGroup = true;
	if (groupConfig.usesTimeConstraints() || groupConfig.hasTimeConstrainedChallenges()) {
	try {
	    updateClock();
	    GM_log("DEBUG: Flickr clock is '" + calculateFlickrClock() + "'");
	} catch (e) {
	    GM_log("Exception: " + e);
	  }
	}
    } else {
    	groupConfig = {
        	groupname: function () {
			return /.*flickr.com\/groups\/([^\/]+)\/discuss/.exec(document.location.href)[1];
		},
		groupId: function () {
			return GM_getGroupId();
		},
		extractChallengeDefinition: function () {
		},
		storeBumpMessage: function (message) {
			GM_setValue('bumpMessage.' + this.groupname(), message);
		},
		bumpMessage: function () {
			return GM_getValue('bumpMessage.' + this.groupname(), "");
		}
	}
    }

    if (!$chk(username) || username === "") {
        GM_log("Sign in to your Flickr account if you want to take part in challenges in this group");
        return false;
    }

    if (challengeGroup) {
	ucpAddCPheader();

	groupPreferences = new UCPGroupPreferences({ groupConfig: groupConfig });
    }

    userNsid = GM_getGlobalNsid();

    try { 
        topicListingTable = $('Main').getElement('table.TopicListing');
    } catch (e) {
    }

    return true;
}
        
    // if copy any of this source, please use your own flick api_key
    // you can easily generate one on http://www.flickr.com/services/apps/create/
    var updatingIcon = 'http://www.flickr.com/images/pulser2.gif';
    //var defaultCheckIcon = 'http://www.flickr.com/images/icon_check.png';
    var defaultCheckIconSmall = 'http://l.yimg.com/g/images/icon_check_small.png';
    var errorIcon = 'http://l.yimg.com/g/images/icon_error_x_small.png';
    //var infoIcon = 'http://l.yimg.com/g/images/icon_info.gif';

    var images = {
        down: 'data:image/png;base64,' +
            'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVQ4ja2TT0hUYRTFf9/33jzf' +
            'OM6kDqOjJBUYBkm1iQpcFLQJgsJWMbWLoha1ipbt22dR0sJNGLQMA0GCTNSysmyypBBRZxpzNGdM' +
            'nffed1sY9G9qY3d57jnnwrn3KhFhI6U3pP4fBip6bqA7MLlGrQVlBIyBQBBjECNIEEAQIN8x/HUc' +
            'EdpaN2fseHVLtLWypi26p5bBD1+x/AA8Hyn5mEAIJWLoiIN2HZTrYICYA8edIr19r57by81B6vXE' +
            'zPjeoKYOhLmCByUfs+qBCBW1CttysNwwViyMCUHI87l+r39uOm8f1bkziYWlLTuPDQyN+TuSDpGw' +
            'jbY1lq3RtkYDSgQlghghAnx+8MTkihUpuX84owHyF5KD+dls95Kv2V4fwrI1ohVKK1DrYQkQchQq' +
            'Pc385Gzn2t1Dvb9sYaUrdepl//CzsGvTFLdRWoNWP9IOWYSzi8z0jwzbia3ny65xber9lRfp6dmm' +
            'uEN8k4Wo9bZRmkipxKdHT+elqr69ePuAKWvg9Vzu+zL18eqbiWzQkqwg4moCUVRaUBgaZVXHThdu' +
            '7Z/5WfPHIa10nezMZOY7lkqK5gYH19Ho3CILmbk7hRv7en7nl73E5ZtHLo6OTT6uiznsrjZk0u9G' +
            'nOS2s+W4djkQQMUbTrwdL415+WyVjje2L17bFZTl/esbo5fSB8UzbrGj9eFfB230nb8B9+jhZWYd' +
            'CrIAAAAASUVORK5CYII=',
        up: 'data:image/png;base64,' +
            'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB/ElEQVQ4jaVTTWsTURQ972OmDZOZ' +
            'pEk6SVvbJqTYkhSFQqG4UKkVrHSjCPoDiqAbkYAKooi4cO3CjR8UseAvcCW4EMSiiwpBiKlUaRXb' +
            'OsbQjFNn3rznoqCIjQZy4e7OPfecw71EKYV2irY1/T8C42z5iHnuzcF/YUgzC+aF1wN5rbMccOat' +
            'EjFavzay0bKCeGmR0VA9GBkzzEMTps0ovd9MwY4EKhDXs/nYgURPDJNDBqaGozPxG0sXW7JgnlmY' +
            'TvXFHpPBNAwI9MKDyQQqNSpWN9Vk7VLuWVOC6OkXfVas45U1PpSpfxeQ3xr4sVZHymIoDhpYXMey' +
            'G2L8S2nA+ctCdPY51YmaS4zlMq6ugyi5vUFKOPUQK46Pgs1ylNE7O2fgB1fTe/unvEwcSoS/EVJB' +
            'SYkVR8DbEtidZMeStz6d/4Og49TTw9251BVV2IXAVyC/0gSUVCBSIRQS1TUfFpfImPRm4vbnCQCg' +
            '5PiTnnzWnNtzdJiGAAglUGS7JQApJEIhIYWE64VYrvmYzkb0Yrf20L630cVP7rMfzezv701mCe5W' +
            'XFQcCbkVQIY+FFMILB2UERCdAxpHOh7BiUIE9oKf//jOn+eRTl4uzS+9L452oVr9ioarQDgH0Rmo' +
            'zkE0DURjIIwBjOFDI8Tlly7ergfwONtsesqtVtvf+BMch8Tnh4oYeAAAAABJRU5ErkJggg==',
        viewmag: 'data:image/png;base64,' +
            'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACFElEQVQ4jZWT30tTcRjGn+85Lo7T' +
            '2XJ1PG79MH9gBI2xiIggatRVRasbQxndRGBRQpdGF0KIF7srm0iLCKI/wLqREKHIm4ygoqDmNsd+' +
            'lLa1zdX0cPZ0sQyjnWAPPLzw5eXDA8/7FSSxrsHBa3Ik+qm5VCypirLpW1dX78rk5Pga/iOxDrh4' +
            '6Yq2UshenpmZtaiaRkM3oLW1Ojq79zwI35uYMyWQhP9cn8131BciuURymWSY5BOSU979h+/3ne8/' +
            'QBK1LAkhpHQyfn0i/EgBkATwEAABFAGk5l+9cKyW9VsnT/mlWgEkAI0tW1y9PZ3aXgAzleobAOwA' +
            'sB1AqkxrOZNOOMwA2/Z5DtkA6ABaJKBZB6wAPgJIA+hwunbLsmxRzQBLAroKYKcB9GQAuwX4UQFO' +
            'AHgJQHW5tG5dX82ZAX5OP338NrOGVhnwSIC3Up2tAILPY2grZhcLkiR9MW0BgDYWDMW+GvxL0RVy' +
            'LDjOQGBgxKyFht+QjBDi2OyzqZDnoO+40qjKhlGiUcrMj46OJPoHBm5GFmJH2p3aWauifP8nwUbb' +
            'bDYFQEdTk3Wz2+2WSMJisUyfPuPnu/cfFkju2rhfM1Yt2+32uUDgAj9Hosu5fN5aN4AkNKfzzdWh' +
            'IebyhVQ8vqjUDQAgVFV9PTx8gyQjyWS6vaFmNSZileLdqqrzxVIpcffO7eyf31iPhBAyqjdk/AIE' +
            'L4Xptl+gaQAAAABJRU5ErkJggg==',
	edit: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADbklEQVRYhcWXS08TURTH/zMtBVuI' +
	    'IbKQR0oiwWjloQmpulEsogsTKsEAAQsu+hVwxwdgQ9y5wY/AhgQHqDxiAKMwpRBtKaA8pJC4ANqm' +
	    'LWnovS6wOJ3OnU6LhpNM7jzuPed3/vc5HKUUF2n8hUYHoFd6OTIy8t9kaWlp4TIC/Kn4z4ISQkAp' +
	    'xejoaNo3JkA8HgfHcaCUguNOoaWl0jsAoJSmXISQMwAl09+6fgOdjpc4jh0jGo2C44DGR41nDeQN' +
	    'k1DSe2kpBSKEgOd5RT9JYw5CaQMpDOteWiZBeJ4/K3U6XXYArCBqQPJ6coisALIJqAYhBckKgOVc' +
	    'KZCW7mBZTgpkq0ROAJmc5todmgHk2aoFTCQSODg4yAmCuRDJYVjzf2ZmBvv7+wjsBtD3uo9Z71wK' +
	    'yLOglCIUCuHN4CCam5thNpvR+6o3LWstCmjaDeVOw6EwFhZFPH7yFBvr6xAEASUlJapjgmXMLpA6' +
	    'kUoZDAbh8XhQaDLB51vF+LiAgYEBxUyl3XEugGTwWCwGt3sJl4xGbG/vYG3Nj/7+fhQXF2cMRAjJ' +
	    'DkAePBwOw73kQUF+AXZ/7mJtzQ+7vQUVFRXMzUmLaVqIIpEIRNENQ54BgcAefL5vaG19DovFknGF' +
	    'VPIntYxdEAqF4HYvQa/XYy8QwKp/FT09DpjNZtXpqVUJBQX+NggeBSEuulF/5zYOD4/g9XrR3d2V' +
	    'IrvaVq1lNqQBJCvGYjEsiCLu3r8Hv9cPj8eNjs52VFZWKgZUCiz/pmQpXXC6rJ4gHo9jbv4TrFYr' +
	    'RFHE9PQUOtrbUVVVlSa31M4xCCkIoTg5OUFenqE8Go3BarViZXkZkx9ceNHWhurq6ozO1LJVV4AC' +
	    'iUQCBoOhtK6+TrA1NWHZswKXawJOpxPl5eWKTuSZ5qzAaSNcq62rn7A1NdUsfFmAyzUGp9OJsrIy' +
	    'zVnlMg15juMRiUSuFhUVTttsjRav15cQhFHY7fa0zDNtzaxSdTMyFZpK8/Pzv5qMRvPYe4F7NzS0' +
	    '0tDQgNra2pwOJNlC6Le3Nls/z85deWB7iMmpqR/CmNDucHRvmEymlIryM7/az4rSM8v0vw4P3n6c' +
	    'n332fWvz8k4g0EUI2QGA4eHhcUopl+lAIYVgPQMAz/MghCjLUGQ0ouamJeWd9EdCp9MxnSoZ6zin' +
	    'dP0Gjp1i+u8w+ggAAAAASUVORK5CYII=',
	save: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAK' +
	    'T2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AU' +
	    'kSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXX' +
	    'Pues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgAB' +
	    'eNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAt' +
	    'AGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3' +
	    'AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dX' +
	    'Lh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+' +
	    '5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk' +
	    '5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd' +
	    '0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA' +
	    '4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzA' +
	    'BhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/ph' +
	    'CJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5' +
	    'h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+' +
	    'Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhM' +
	    'WE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQ' +
	    'AkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+Io' +
	    'UspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdp' +
	    'r+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZ' +
	    'D5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61Mb' +
	    'U2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY' +
	    '/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllir' +
	    'SKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79u' +
	    'p+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6Vh' +
	    'lWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1' +
	    'mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lO' +
	    'k06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7Ry' +
	    'FDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3I' +
	    'veRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+B' +
	    'Z7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/' +
	    '0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5p' +
	    'DoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5q' +
	    'PNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIs' +
	    'OpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5' +
	    'hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQ' +
	    'rAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9' +
	    'rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1d' +
	    'T1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aX' +
	    'Dm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7' +
	    'vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3S' +
	    'PVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKa' +
	    'RptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO' +
	    '32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21' +
	    'e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfV' +
	    'P1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i' +
	    '/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8' +
	    'IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAEZ0FNQQAAsY58+1GTAAAAIGNIUk0AAHolAACA' +
	    'gwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAISSURBVHjanJK7TlRRFIa/febMOXNzUKkE' +
	    'Ejt8ACtNaMTLIxjtTXgDOwufwNqCJzASTSQQE6NigEJCYmVIpBNIwHgiw5zLXmsvi5mRAGrB3+7s' +
	    'L/9luYmJqdtew/zs7K3o4YP7BeAsGH9T5JypBbe8/PbC2vrnfHNj7WrcL4rO2Nj4VLfbdffu3iFJ' +
	    'Uv6nX1nGwsIr2t3LAMQOC0mSqlkU+0oQBVEF3ImPZkaaJgQzzMCX5QBgBpWvCCqIGT6vEFWcOwsI' +
	    '5lAzNCgadAQwVBUJgb4P+Mqj8g8AjhAMVUO8HwDCEKAhUBRCkReoKtEpQDBD1HDOETQgXgYADIIo' +
	    'VVlh5mh3uvjh42kHjWaDo6NDKl/hRw4sKEmasrX1ladPHtNotIYlgvtTpGFAHMf0j3psb38jSdPj' +
	    'CHG9htcaHz+uU+R9osiR1tNhWQEIqASiWkTaaJKmKWlaGzkwxAshBJqtFjM3rzN9bRo1aDeb1Osx' +
	    'DqjHMRubX/jwaZ08zwnDY4sNEPEc9Q5ptZrMzT3ixcvXLC69Y/75sxM9lCK8X1nFe8FF5fGMIgJR' +
	    'RVVGAOzu7LG3s3umyOxnRlVWqHosxKMVQqyqMd4j8SBXr9ejyHtnAL4SxAuiitkwgpdqvyj6Wb+3' +
	    'f7HdafJmcYmZmRtMTl5hZWWV8fFLqCqdTpssy/hx8J2yyDkgGS41iCGcU25k5bz6PQCXUFUR3b1h' +
	    '6AAAAABJRU5ErkJggg==',
	sticky: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACPklEQVQ4jaWTy0tVURTGf2vvfW5S' +
	    'lnI1KzIhg7qRZpT0EiIhp9GwQeMgmjRp0rB/oEFB0KwggmgS0YMeOikLNEXBqwmaPTSs64Nzw9O9' +
	    '95y9Glw1ehBJi7XZ32Kt/a31sdiiqvyPuVv3u7KrXJABxKOU6bTs5QtZLDZGEJFyLILXZNQVYl+3' +
	    'f2+zABTiGO8VRIhLigp4wABiwIghsBAIWGt42T+40SUYnwu/ApAkHlRJ4hI2CFCvOGOI4xJiHcYa' +
	    'ioUizjkQKHnxTkVqJifeArBSCSpSxfVH3YkuWmfPsK4E3+7s9YZ/WIL6hO4XT8g+ugTAWM+VpQRy' +
	    '48HzpH59jfkbQX6qj+Mda8hNjfF6oplU1RYAcmGoRlHaWzO0t2YAfsOHMhtYF3XDt/fUpgOSQp4j' +
	    'exppb82gAs5a+8euMnqNgY+O1UHI0YNrIXwPPk2TPGPg5jOqa2pJ5StxItDVO7L8cAlXLgzR0rYb' +
	    'WFtOhPPg52nYKDSsDgmzfcxqB857vzxyV+/IMn4Ynmeg8ypNdZPYhubFPQJzWWYmE760nKXa78JY' +
	    'RKIoIoqinyRUrKsnSG/Fzn6GmeEf58MU05VtZA6fAlWcMRq/GnoX/CoBYGg8z47tdViAgXFoaSRJ' +
	    '1zE0mme6dwRRTdzsTO5eKinuQ9lcTGL1Hrz3KJBSNXaNs3efVjCTOubTTyY50YHZOd+jbz69i2aj' +
	    '0mP523e+dSZ1Ogrtjv65b5cvP9CJcyekYlulPbnJ2AODC8ULF+/o3HdAEz+jITZpfwAAAABJRU5E' +
	    'rkJggg==' /*
	    'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAD1UlEQVRIibWVS2xbRRSGv5l7bSdx' +
	    'nKRpnSdJGkhDIbEjISoRtagsaNVFBQskxAapQixYILYsWCBYsIJF2SMoD4HUimdJRaUmBEofFEKc' +
	    'SG0DNOkrb9txIju2e+/MsLi264iEllT9dRf/3Kv5zzn/OXdGGGO4n5D3VR2wP/1+8KOFxeR+11WO' +
	    'NgZjNFobjAGtNbpQodIGjPZ2CYkUIIRASoElJX6fjTEGBNiW7WtpCn/7woG9r9hLieTug0/1N+cd' +
	    'hdIaVymUq3G1RimN0hqlDForlPIELCmQUmJJSU/XA1ybibOjo5nphQStDVtZTC5x8udzewBs5Tj3' +
	    '1ITLUzNIKZlZTAIwu5DEtgVor1pbWILOljCWXSgRUXw2BSEEruMgCgI2gGVbjI+P30sha9DT82iJ' +
	    'r5mi3t5eensjAEQiESKRzfFy2OULITZrzMZYE6DcpjtxYzQXjr/G8opFY+SlDS1eY9HdlB+NRolG' +
	    'oyzd+JXH9vay64lWlhcnS+//M4AHzyYhRMmycl7M3l79HZ/IsaWhCbl8ft3soWiR+bcVY2NjG/K5' +
	    'K+fZ/YgFKw4EVtnZFeDsj8cJ1revH8AUIvT19QEQi8VKfOTCaba3biM+P8Ol0V9oDDp0+y9R7WuE' +
	    '/CqsptkW9JOZOcGNi83Ecz5C1hK+W3OBr98I1xYq2Phnzlz5At/8RbqDlXT3haGqGkQLGB+4CrQC' +
	    'I+loD9MRmISFeczYMM/rbNex2fyrNprSgRaLxUrCRa5CUSamRnk8ugWsSpAVICS4WXDTXgAscA3Y' +
	    'DlT7EYkVBidSubHpzNCaHqxnUQyo2f4gseF36HsoA9UGAgFQeXAdUMo7d1wFORfyFfyRq+Fq+77p' +
	    'w++OnrHL9DdEZ8/TTBk4893r9LdlELUBb/60Bm1AaVAadUvzW7KFtoNvETg9mSs02ZQOtvUsus3D' +
	    'pFoPoBIfYusABCwwwuufMmAgm9Gk256joS0KXPWmSNqWiCeXCDc04WqN0QZtTOGyobDWGCBUYWMr' +
	    'DVnXy94I0LeHJOBotobqSMXTSOHlbTuITz748sSTSmllChNlinsK3hm8G65nbiBCvW4hryCnURkH' +
	    'iUAE/SA1tqs5e+rzv+KX9fWaUNUpAHG3l/77L8uq/oqqH3a11u5ROcHIzeWbQ4nsN5UWwX3Ntc/s' +
	    'bKmpRzoMXUt89lM89+KbRz1h+07CRQhBQ530dU7MOqmT06ljf2edI5aPEWnwfTybPPrw0uqh/XXV' +
	    'z9a4codrCALp/xXA+E1mMJN+73rS/TMnGD48YNJlnwfePiTOHUnlvvK5MuR6nQHgH0wf5+oUM4WQ' +
	    'AAAAAElFTkSuQmCC' */
	    ,
	queued: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A' +
	    '/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wECgw2KIam8LsAAAK9SURBVDjL' +
	    'fZJZSFRxFMZ//ztz73XSbNCZQrMdpcDICluQJIiiosJ2iOohKoqQioiKKDIKiuhFK+qhjQZNpcVB' +
	    'bmn0UKEt1GhGCzGWmblMmdR1dMbR+fcgI2rW93Q43wLn4wBQlDmXCKSUfXOzu2RHs7tk11DcjfQ0' +
	    'AKwA6x4/pfGeMcMixHIhRI6UEiEEnbXew1KGAc5Hdt/Ly46HkLcSFy1+DaBEEhuKXIVKKHTs04W8' +
	    'IiEEAN3mbxk2TQCEEHw5l2vQ4T/y9abLFfH1BRgF+QXfSorRo2PWfDxzshCgx2/KcHtvwKcjhwxV' +
	    'ysVN7tuUFbhuMRj7otD22kRe9fbN0nevVO6HuEfpKV8q0lPqT0RbHa3FhbJmy0Z50MalA1FoEZ8Y' +
	    'HLRH5YRTJ80bYLXTwmwFLAHBs0QLxs8QnlNd7OuvF97dO3P1OOdKqVqk0DUC5s+Hc46d2doKPf2F' +
	    'GWC5sn/32egR8avCgU6CbhfB9rYH1uY31RmO0eOS0DWEquLzvl8x2AxQAT3fnlcuS5yUMibc0oh0' +
	    'JtCm2jJFjk62TZIlQSKhXVfuZ1e9uzEyeXJL/4C2z95Rp1OTV9hDci0gEChBeDCgA99Mu27GTvZp' +
	    'abNiy/IvDs966BEQxlg+XyzYvONXsMZjjmipdsZXNgf/KvHp1IQYZ9KUVotjpPbZU8nqt/UxL7KW' +
	    '1gBi4V1j2pW0Sb+TszbQ9bamy+d9FT+3qqF9wB/EZSwptyYkqR2vK8JGbf2mNvDLseM1OXa8Wgem' +
	    '8a52i7+sWKrjJqr2WYvcEZ81MtSVlwYm6BrX6xo2RSvkAwybl9lL5l5gmIVrl6s+yG2//FfrOwPd' +
	    'f52QreGwKiTEKrzJ6ejd/fC8/IpQcEyfMQbgqA1hSlK7wzTldfGjr7zDNobEbYf2+I5TfzIU9y/P' +
	    'AKwX2NcJ7P/T/AEtxhn4rk6s8QAAAABJRU5ErkJggg==',
	update: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAANkE3LLaAgAAAmhJREFU' +
	    'eJyVkk1IVFEYhp9zz517xxnHf8RGG0H8GYYKCiEiCMWkItq2diUE5aZFf8sWEVQE4qZFyxZCbVpE' +
	    'RIug3JiKhKEzlalkpkwzNnPnXufeuaeFY/lDRc/yPd/78PFxBDsJxgZiD+OJeKfruptJJU0AUtdZ' +
	    'WPy8nLqdOgVktgr6LkGop7en/9LZiw15J7+Z+OUHM8To+GjT3fv36tn4swDXKzjfC6tYjrUjd/wK' +
	    'nFJ+A1Db870CYbPupbE9C0Q5VFDUKnCxdo/vEXietMmX0tjK/t1X4JYKuJoFAm+3QAJhAEzqPJkT' +
	    'edIUcbYvgIdJUeYEJrU4ZNjczxLmUQYPX+i4WaWHhJK+aItFa/fXxaTCQysbfAUoybfcipqbX8xo' +
	    'rizlsZl+lBzWNZP2cNRpTDQeJBww8SlisYqmgyZUWSDwPUFVZUh0H0jUpe3vfFifQZh+l27PMjb3' +
	    'Zsk1etcDLbUJQgETXYeAriM0HZTC9Ys4yiFTzLJirWHbaZanwJrklcRiNv+Rr2bLRl9VW84oGFnQ' +
	    'PAxhIIXEVx6Wn2XNm2fVXyKoFcm+U15qhOveJ0YkgPrBZGaGLzUdpf7W9hrDkNVEtBoispagFkKK' +
	    'AELTqDEM0tOOO37Lu7qR5M7mZcr4OaZXpvxUQ9w/3R0/YjbqnTTozVTLBkwtTHUwQGoy6b64lhty' +
	    'Ugxv9X4JAEo53i9MFJOReLrvWOJ4KCoPEZH1BIMur98+LTy5vDJUSPJgz2/ajRHl3OCz5rUZ/7FK' +
	    '+s/VlbFEPtLFwD+L2zGjnBx6uW/1xkSbVdXJ+f8qbxFs4kSolTN/m/kJ0mH+2AHDp/kAAAAASUVO' +
	    'RK5CYII=',
	lock: 'data:image/png;base64,' + // lock icon
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ' +
	    'bWFnZVJlYWR5ccllPAAAAGNJREFUeNpiYKAFYGFn9wfi+0D8H4pBbH9iNdsjaTwPxPuR+PbEGHAe' +
	    'qjgezUVgA4kxAKtCmEvQxZlwmPOB2PBiombI5yMFFiGcj80FAiTYJ0A1L4wagGrABxL0faBa+gEI' +
	    'MADz0iKXhglKDAAAAABJRU5ErkJggg==',
	random: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ' +
	    'bWFnZVJlYWR5ccllPAAAAJBJREFUeNpiYBiKQB+Zw0SGAQFAPB/GYYHS9VgUHgDig1B2PxAXoMkL' +
	    'AHEgjPMfB9aH2oZNbj6yCxzQTDeAOhVkSwIQf4CquQh1rQIQJxLyKz8Q34faFo8rEPGBeqjm/fhi' +
	    'AQbs0XA+kl/toS7RRzJ4ProBuAIxHs0wnIHYQCAaFXBEI1kAqxfITsoDDwACDAA+SzAr9DWWrQAA' +
	    'AABJRU5ErkJggg==',
	reload: 'data:image/png;base64,' + 
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A' +
	    '/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wEGBMyHsAa238AAANdSURBVDjL' +
	    'bZNdaFtlGMf/56M5yUnSD7ulTT8SXJJuxbU2lipeFHGMKGWGiei8Gc4JcwyEgOxmF8NKBxNFEOZw' +
	    'VxOFWbeJTlhFkbrRidbaurXpR7amadJ8tcvycc5yknNOzvt6Zahj/6vngd8DDzzPj8FjsnNHw36X' +
	    'xxZwOERvtaop8cTD6fh69UdDpfFHWWZ7I5pZ1/B+17kjh99+pde9G6S0ha3EbWxt3sWdjVT221/S' +
	    'o8mY+sX2Ga5esEzLy0HvtU/OnN/nf9IPObmEYvwfsEoG3WYDg52CrdvFHJiNKbGyZNypb/BUvy3k' +
	    '3bUjQFFzhE6cGvS1e7A+P4l8dgnh1FqtWFFK3U1c67PtZlg5BpeWcrmPvtx8jlTJGgBwrx70XXrv' +
	    'tTeH+tpaOiyVPAqJv1HOreL6Srh87pvYOzf/KL4/tViazkD1+trNzn6nKE5F5fyDXO0mAHAOJxfo' +
	    'czT5eL0ArhyHzUihkZPQKIKLF7WtREK9olXIciSqjy/mlea93ZZnVBDz7ZXKV6Ag7OJyYSKdW4Ud' +
	    'ZdgaNJhYAo5l4GkVOIuZ6QTLMABAdUOKhCufX1mUshDQC8AGAGwyoV2eiaU2BK4ME6MBPBAuKTj1' +
	    'ffKn36bkEAilopntDYzsuXjh4tnfx8YmnD5/sLHJyg7UT+HptYx+dtJNx097tRNvOdbaXKaTYBkT' +
	    'AHR0Ci+Njr2R2UwtUE3OUTkbpfN/jtPnh1u/AwAeACoyndkw9eDrX2duzE5Lh6hBCgDQ0MA4jx0Z' +
	    'Hj+8L9icnb+BqHwfmlIEbyh4wd82MjtT6OMAQFeJcfzY0eP+gf7Oubm/UlLJmAUAhmEYpxMHRXXD' +
	    'yRaWwZYiMKvrsKkpiBaNv7UqxzgAqBlUsljzLx4NHupxd/EHpGpm7wNJkwgLnnBq1+4u+2CH3QIL' +
	    'qwCoQmFqMEBw697De/VXbu8wvf5hKHB5wOFCspBAJB2Drqnanp2NJrddAAcdAk9xdSVLJuakSYGH' +
	    'Gl6ofLzdBdY/ZD0fCva8628WwNIaqKEDHKkDk2kJn17dPBNfU0+DUPI/FwDQbLb2c7RYahGbak/b' +
	    'rZTneUBlCNZlDT/cLUgXruXOJmPqB6Agj7Xxv5hs7NAutzDyRDPvMQxqZO7rkXRKv16rkoVH2X8B' +
	    'DhSMbKu1q+IAAAAASUVORK5CYII=',
	mark: 'data:image/png;base64,' + // black cross
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAANbY1E9YMgAAABl0RVh0' +
	    'U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGkSURBVHjalJM9SEJRFMd9+tQSIj+hIXBx' +
	    'qKGmoqkagogQohwCIWhKgqA5iMYWg2iIILAhgjD62htbaojIPV4OEkUtJggqz34nrmXqM7rw49x3' +
	    '7vmfd95552rVatVWW+FwuAvTAW/ZbPbnoM3SJAHCEHuvpmlRrIkvhS1YaIJ2u13irgzDyOnKOQgn' +
	    'CP3YCrZEsj2LBIumaa5zvsz+2K6cUoFfqvF4PLrL5dpmP9cg9MEpJKGb85g4awnOYQgSlUrlPRAI' +
	    'uB0OR4qgEXUucTsQU885OKtPUII7ytovFovRfD6f8fl8XnxH0KPECyr2Gqal/PoE34s33xQKhSlI' +
	    '67oeUYIVaS5swARkavF6qy7R5WcqiWNdPM4q8RKkmmKt/i/iAcy4EktcpFWcbqEPqe+XCmbUs7z9' +
	    'Fi7/StAJaQjAGNwrv0zoLjyA0fQJ/IEvWIfQC6N1YlkyWBew1dQDETI8Nrq+qeZhEh5bVLcGwzDf' +
	    'VAHjGWeI+pX4yaI3HwzXgdPpXP3VA5x95XJZtgl4bXv7NC1JrLvxNgZlhuDF9o/Flbd9CjAA9aSJ' +
	    'Cs6w1hQAAAAASUVORK5CYII=',
	info: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACi0lEQVR42l1TTYhSURSeq0/fjAyM' +
	    'otL4k1AwuowBoUIZG4bG2ZSLdtKqgmnTahaCYFGuCmcX2CxqY7gIooWbHIhy0JqBKFoEapGmhiJP' +
	    'ZMD/374zvFdvvPBx7zs/3/nOue+yudPLAGwBHuACYAH+AN+AFPAWEOQJTHb2KBSK29Pp9ArOA2AM' +
	    'TAAFoATUjLEPk8nkuUj2nwAODxDAcQV7jxLVanXJYrHs12o1T7fbXSEikM9j/4H9MZASc5mB47hd' +
	    'GC7j3BUrK71e771EIvElEomcCQaDb2AbkR1xC4j7NBqNdnAWmEqlugnHQxhbAAXNwQEB6ietVivG' +
	    '8/w6vp+KysjHAYs4PhgOhy+ZRqN5hr7W0H+f/GKQajweL8IuINEIhUQ+kNqGnUf8QafTuUsEB3Au' +
	    'AWNpMCBQQlnZaDQmG43GRr/fPzfrB45BsEYEOVToIGAqBaAC5/P57sRise+BQMAWjUZfoeJQRsAw' +
	    'Aw0IHEyn09E0lyT5tCB/3mw2b2Wz2d9Op1OXz+c/KpXK3sz1HzebTQ8zmUx7g8HgEhRMJC/YF7Ra' +
	    '7WaxWCw5HA4trvIQLXVlChQY8mG1Wt1mdrv9FvrcQYV/EjFdIrhaKBROCOr1+pGcAApVer1+F8pe' +
	    'ML/fb81kMo8wKKekggiwrpfL5ZzNZltut9spVOxK1XG1n10u1/14PF5hZHC73ZuVSmUbiedpmNh5' +
	    'EP5Ea3EkXkPCKhT0aXjYf1mt1r10Or1PBZnIym9glUqlG/htL1IgkUAqh9ZGlEzEUHUERa/fYeG7' +
	    'f+oxEUk4HD6bTCbXBUFYBZEDw1zGFdeQmDMYDF/xe78PhUJlKXn2NZ70h40eDA+oAE58AzRgSurJ' +
	    'b4vWXxbUXSASOlGMAAAAAElFTkSuQmCC',
	bump: 'data:image/png;base64,' +
	    'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAANbY1E9YMgAAABl0RVh0' +
	    'U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAMbSURBVHjadFNbSBRRGP5mXdsd75q6XtAt' +
	    'tUiK0tKCohIMSrAU0S5UdHmIXutFqYegjKKolyCKqKCkDMlICilN6CKB6IqatgRmlm66lxn3Mpfd' +
	    'nZnTmRH3QejAxzlz/u///u//Z4YhhGD56t9+aYO1IOV8NCBuiywIJpkXh0Ke4O0G32PHci6zXGCw' +
	    '9uaZ1IrCq0RQMsXvblARqGoUwrzf7+f9Vxp8j279V8BRe+NR6tbVp8xsEtJ2roW1IIMyGIRneMw9' +
	    'H4Dr1QC8QU9Ho+/JwViSLqBjsKq1eezkPeJ69pUogkz05Xs3QbxvxoyzKobJfOcQ+VjaQtpXHru2' +
	    'lGc4GGm6vUoLqc78s9WW7APlRlXP+3F4u78BJgYZu9bAVlduFPT2jmP49H1lIcRVNHFtI2b9Ugsr' +
	    'F9l1hZasfRuN5LnuUfy80xNzyU38hqKqyG+owMrdpSg8usssPOxupaH9hoAaF1dvaygDUQh+vfyC' +
	    '8cudILQyUbXFQZlM4FraIPr8KDq8A7lNWzDbN1ylx0wGISclLcmeCb/zD4JTc9RuGeSIhADHGRDF' +
	    'IGy0NeEvB/7bNFhbCpI2FbB67mILVoYw0JBebEP6+TpqV8PPXgdEr08fMxKsVpRdaMSKBAsQjkAN' +
	    'itAsRu1FB4LHP6OGJCASNQhECoN3z0EmMhSzRncJSlCgsbDBUUUJIhfgYgLz36ea3ePTiwQ5DEIJ' +
	    'UUGG2WyGtTAZFnsivZNpLGLEuR8ueJzT12MtuCZ/dw639/3KWW9fZWY1xGka7Hs3IyJJiM9MAJuW' +
	    'jHi9FHWmUhHH8163a3L6bszBuYUudeLDQN3Qi76IQkmaFEXIzSPgpgMMBCGFBGg0UY+NdPWro2/7' +
	    '62mObDgozrUZLwIsnJ47odqAj2+vPFSdkVdZAn7GbXwXiWwipICA4defA90PO050sk7Hg1wbo0+Y' +
	    'KcrJ1g9WijyKklRiKT9SWXO8umZPic2eF69re/7MK596+qaefulq40zyIOVNUsxSiLrA0iwSKdIp' +
	    'sqhiFmGI/iclgRj+RLrTqRMPfaC2wFOEKJR/AgwAlWOjeZLZz98AAAAASUVORK5CYII='
    };

    var applyConfig = function () {
        var groupname = groupConfig.groupname();
        var somethingChanged = false;

        somethingChanged = groupPreferences.setHideNonWonMedals($('showallmedals_ng').checked) || somethingChanged;
        somethingChanged = groupPreferences.setStopAtFirstFoundMedal($('stopatfirstmedal_ng').checked) || somethingChanged;
        somethingChanged = groupPreferences.setSmartPhotoBoxes($('smartphotoboxes_ng').checked) || somethingChanged;
        somethingChanged = groupPreferences.setBumpAndReload($('bumpandreload_ng').checked) || somethingChanged;
        somethingChanged = groupPreferences.setBumpAndReturnToFirstPage($('bumpandreturn_ng').checked) || somethingChanged;
        somethingChanged = groupPreferences.setDefaultAwardsGroup($('defaultawardsgroup_ng').value) || somethingChanged;
        somethingChanged = groupPreferences.setDefaultAwardsGroupInPendingItems(
                                            $('defaultawardsgroupinpendingitems_ng').value) || somethingChanged;

	somethingChanged = groupPreferences.setWorkflowAutoClaimOwnership($('workflowautoclose_ng').checked) || somethingChanged;

        if (somethingChanged) { 
            window.location.reload();
        } else {
            togglePreferencesDialog();
        }        
    };

    function togglePreferencesDialog() {
        var configDialog = $("UCPANGConfigDialogDiv");
        if (configDialog) {
            configDialog.destroy();
        } else {
            
            var table = new Element('table', {
                border: '0',
                cellPadding: '5',
                styles: ucpDialogStyle
            }).adopt(new Element('tr').adopt(new Element('td', { colSpan: '3' }).adopt(
                new Element('table', { border: '0', cellSpacing: '0', cellPadding: '0' }).adopt(
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            valign: 'top',
                            styles: {
                                fontWeight: 'bold'
                            },
                            html: "Admin Preferences for this group"
                        }),
                        new Element('td', { width: '100%' }),
                        new Element('td', { align: 'right' }).adopt(
                            new Element('a', {
                                target: '_blank',
                                html: 'help',
                                href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623759768824/#comment72157623761169572',
                                styles: {
                                    fontWeight: 'normal',
                                    cursor: 'help'
                                }
                            })))
                        )
                    )),
                    // smart photo boxes
                    new Element('tr').adopt(
                        new Element('th', {
                            colSpan: '3',
                            noWrap: 'nowrap',
                            valign: 'top',
                            html: "Photo Information Boxes:",
                            styles: {
                                background: '#CFCFCF'
                            }
                        })
                    ),
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(
                            new Element('input', {
                                type: 'checkbox',
                                id: 'smartphotoboxes_ng',
                                checked: groupPreferences.smartPhotoBoxes()
                            }),
                            new Element('label', {
                                'for': "smartphotoboxes_ng",
                                html: "'smart' photo information boxes"
                            })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic',
                            },
                            html: "When selected, photos that have not been approved yet, will have their photo " +
                                  "information box shown automatically"
                        })
                    ),

                    new Element('tr').adopt(
                        new Element('th', {
                            colSpan: '3',
                            noWrap: 'nowrap',
                            valign: 'top',
                            html: "Awarded medals:",
                            styles: {
                                background: '#CFCFCF'
                            }
                        })),
                    // non-won medals
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(new Element('input', {
                                        type: 'checkbox',
                                        id: 'showallmedals_ng',
                                        checked: groupPreferences.hideNonWonMedals(),
                                    }),
                                  new Element('label', {
                                        'for': 'showallmedals_ng',
                                        html: 'only show won medals'
                                    })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic'
                            },
                            html: "By default, all the medals for the chosen group are shown when " +
                                  "checking for 'awards in group', even if the photo did not win the " +
                                  "medal (they are clearly marked to see the difference).<br/>" +
                                  "This way, admins/mods can see which medals are known by the script.<br/>" +
                                  "(if there are any missing, please let us know)"
                        })
                    ),
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(new Element('input', {
                                        type: 'checkbox',
                                        id: 'stopatfirstmedal_ng',
                                        checked: groupPreferences.stopAtFirstFoundMedal(),
                                    }),
                                 new Element('label', {
                                        'for': 'stopatfirstmedal_ng',
                                        html: 'stop at first medal'
                                    })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic'
                            },
                            html: "When searching for medals, stop at the first found medal. " +
                                  "This is especially useful when searching in all groups."
                        })
                    ),
                    // default group for awards
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(awardsCombo = new Element('select', {
                                        title: 'Choose default awards group',
                                        id: 'defaultawardsgroup_ng'
                                    })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic'
                            },
                            html: "By default, the current group is used as the group to test for awards in challenge threads. " +
                                  "<br/>Choose another group as the default."
                        })
                    ),
                    // default group for awards in Pending Items
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(awardsComboInPendingItems = new Element('select', {
                                        title: 'Choose default awards group for Pending Items page',
                                        id: 'defaultawardsgroupinpendingitems_ng'
                                    })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic'
                            },
                            html: "By default, the current group is used as the group to test for awards on the Pending Items page. " +
                                  "<br/>Choose another group as the default."
                        })
                    ),
                    
                    // bump & reload
                    new Element('tr').adopt(
                        new Element('th', {
                            colSpan: '3',
                            noWrap: 'nowrap',
                            valign: 'top',
                            html: "Bump:",
                            styles: {
                                background: '#CFCFCF'
                            }
                        })
                    ),
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(
                            new Element('input', {
                                type: 'checkbox',
                                id: 'bumpandreload_ng',
                                checked: groupPreferences.bumpAndReload(),
                                events: {
                                    change: function(e) {
                                        $('bumpandreturn_ng').set('disabled', !this.checked);
                                        $('bumpandreturnlabel').setStyle('color', this.checked ? 'black' : 'grey');
                                    }
                                }
                            }),
                            new Element('label', {
                                'for': "bumpandreload_ng",
                                html: "reload after bumping"
                            })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic',
                            },
                            html: "Automatically reload the discussion page after successful bumping."
                        })
                    ),
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(
                            new Element('span', {
                                html: '&nbsp;&nbsp;&nbsp;&nbsp;'
                            }),
                            new Element('input', {
                                type: 'checkbox',
                                id: 'bumpandreturn_ng',
                                disabled: !groupPreferences.bumpAndReload(),
                                checked: groupPreferences.bumpAndReturnToFirstPage()
                            }),
                            new Element('label', {
                                'for': "bumpandreturn_ng",
                                id: 'bumpandreturnlabel',
                                html: "return to first page after bumping",
                                styles: {
                                    'color': groupPreferences.bumpAndReload() ? '' : 'grey'
                                }
                            })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic',
                            },
                            html: "Automatically return to the first discussion page after successful bumping."
                        })
                    ),
                    // workflow
                    new Element('tr').adopt(
                        new Element('th', {
                            colSpan: '3',
                            noWrap: 'nowrap',
                            valign: 'top',
                            html: "Workflow:",
                            styles: {
                                background: '#CFCFCF'
                            }
                        })
                    ),
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAlign: 'top'
                        }).adopt(
                            new Element('input', {
                                type: 'checkbox',
                                id: 'workflowautoclose_ng',
                                checked: groupPreferences.workflowAutoClaimOwnership()
                            }),
                            new Element('label', {
                                'for': "workflowautoclose_ng",
				html: 'auto-close in workflow'
                            })
                        ),
                        new Element('td', {
                            styles: {
                                fontStyle: 'italic',
                            },
                            html: "Automatically click the CLOSE button in the <i>Claim ownership..</i> step"
                        })
                    ),
                    // apply/cancel
                    new Element('tr').adopt(
                        new Element('td', {
                            colSpan: '3',
                            align: 'right'
                        }).adopt(
                            new Element('button', {
                                type: 'submit',
                                html: 'OK',
                                'class': 'Butt',
                                events: {
                                    click: applyConfig
                                }
                            }),
                            document.createTextNode(' '),
                            new Element('button', {
                                type: 'submit',
                                html: 'Cancel',
                                'class': 'DeleteButt',
                                events: {
                                    click: togglePreferencesDialog
                                }
                            })
                        )
                    )
                );
            awardsCombo.adopt(
                new Element('option', {
                    html: '-- Any --',
                    value: "Any"
                })
            );
            var awardsGroupPreference = groupPreferences.defaultAwardsGroup();
            $each(ucpGroupConfigReader.groupList(), function (group) {
                if (!$chk(group.hideFromAwardsList) || group.hideFromAwardsList !== true) {
                    awardsCombo.adopt(new Element('option', {
                        html: group.name,
                        value: group.groupname,
                        selected: (group.groupname === awardsGroupPreference ? "selected" : "")
                    }));
                }
            });
            awardsComboInPendingItems.adopt(
                new Element('option', {
                    html: '-- Any --',
                    value: "Any"
                })
            );
            awardsGroupPreference = groupPreferences.defaultAwardsGroupInPendingItems();
            $each(ucpGroupConfigReader.groupList(), function (group) {
                if (!$chk(group.hideFromAwardsList) || group.hideFromAwardsList !== true) {
                    awardsComboInPendingItems.adopt(new Element('option', {
                        html: group.name,
                        value: group.groupname,
                        selected: (group.groupname === awardsGroupPreference ? "selected" : "")
                    }));
                }
            });
            // group definitions
            new Element('tr').inject(table).adopt(
                new Element('th', {
                    colSpan: '3',
                    noWrap: 'nowrap',
                    vAling: 'top',
                    html: "Definitions:",
                    styles: {
                        background: '#CFCFCF'
                    }
                }));
            new Element('tr').inject(table).adopt(
                new Element('td', { noWrap: 'nowrap' }).adopt(
                    new Element('img', {
                        src: images.update,
                        id: 'ucpa_groupReload',
			title: 'click to update the group\'s challenge definitions',
			styles: {
				height: '12px',
				cursor: 'pointer'
			},
			events: {
				click: function (evt) {
				    this.set('src', updatingIcon);
                                    ucpGroupConfigReader.checkForUpdates(groupConfig.groupname(), true, function (result) {
                                        $('ucpa_groupReload').set('src', result.stat == 'ok' ? defaultCheckIconSmall : errorIcon);
                                        $('ucpa_groupReload').set('title', result.stat == 'ok' ? 'updated' : result.error);
					$('ucpa_groupReloadLabel').set('title', result.stat == 'ok' ? 'updated' : result.error);
                                    });
				}
			}
                    }),
                    new Element('label', {
                        html: '&nbsp;update&nbsp;challenge&nbsp;definitions',
			title: 'click to update the group\'s challenge definitions',
			id: 'ucpa_groupReloadLabel',
			styles: {
				cursor: 'pointer'
			},
			events: {
				click: function (evt) {
					$('ucpa_groupReload').fireEvent('click');
				}
			}
                    })
                ),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "The definitions for the group are every 12 hours. With this button, you can update them now."
                })
            );
            new Element('tr').inject(table).adopt(
                new Element('td', { noWrap: 'nowrap' }).adopt(
                    new Element('img', {
                        src: images.update,
                        id: 'medalsReload',
			title: 'click to update the script\'s medal definitions',
			styles: {
				height: '12px',
				cusor: 'pointer'
			},
			events: {
				click: function (evt) {
					this.set('src', updatingIcon);
					new UCPMedalListReader().checkForUpdates(true, function(result) {
                                        	$('medalsReload').set('src', result.stat == 'ok' ? defaultCheckIconSmall : errorIcon);
                                        	$('medalsReload').set('title', result.stat == 'ok' ? 'updated' : result.error);
                                    	});
				}
			}
                    }),
                    new Element('label', {
                        html: '&nbsp;update&nbsp;medal&nbsp;definitions',
			styles: {
				cursor: 'pointer'
			},
			events: {
				click: function (evt) {
					$('medalsReload').fireEvent('click');
				}
			}
                    })
                ),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "The medal definitions are updated once a week. With this option, you " +
                          "can update them now."
                })
            );
            $('UCheckPlayNGAdminPreferences').getParent().adopt(
                new Element('div', {
                    id: 'UCPANGConfigDialogDiv'
                }).adopt(table));
        }
    }

    function stripChallengeAnnouncement(challengeAnnouncement) { // used for regular replies too (in Approve)
        // strip everything from the challenge header
        challengeAnnouncement.getElements('h4').dispose();
	challengeAnnouncement.getElements('div[id*=FPAOTP_bar]').dispose();
	challengeAnnouncement.getElements('span.FPAOTP_unfaved').each(function (span) { if ($chk(span.getParent('div'))) span.getParent('div').dispose(); });
	challengeAnnouncement.getElements('div.ucpdiv').dispose();
        // keep everything uptil the first 'small' element
	var smallFound = false;
	challengeAnnouncement.getElements('*').forEach(function (el) {
		if (el.get('tag') == 'small') {
			smallFound = true;
		}
		if (smallFound) el.dispose();
	});
	var children = challengeAnnouncement.getChildren();
	if (children[children.length - 1].nodeName == 'BR') {
		challengeAnnouncement.getChildren('br').getLast().dispose();
	}
	var retval = challengeAnnouncement.getChildren().length == 1 ? // no blockquote, no div, ..
			challengeAnnouncement.getChildren()[0].get('html') :
			challengeAnnouncement.get('html');
	// remove horizontal tabs
	retval = retval.replace(/\t/g, '');
	// remove leading and trailing whitespace
	retval = retval.replace(/^(?:<br[^>]*>|\s|\n|\t)+/, ''); // doing both replacements in one statement makes the script hang
	retval = retval.replace(/(?:<br>|\s|\n)+$/, ''); // not \t ! => unresponsive script
	// remove line breaks
	// 'p' elements are not allowed
	retval = retval.replace(/<p[^>]*>/g, '');
	return retval;
    }

    function updateChallengeHeader(title, message, buttonCell, editLink, callback) {
        // strip the starting 'p' tag
        message = message.replace(/^\s*<p[^>]*>/,'');
	if (SIMULATE) {
		if (callback) {
			callback({ success: true });
		}
		return;
	}
        new Request({
            method: "post",
            url: editLink,
            headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Referer": editLink
            },
            data: "magic_cookie=" + GM_getAuthHash() + 
                                "&done=1" +
                                "&subject=" + encodeURIComponent(title) +
                                "&message=" + encodeURIComponent(message),
            onFailure: function (response) {
		buttonCell.empty();
		new Element('img', {
			src: errorIcon,
			title: response.statusText
		}).inject(buttonCell);
		buttonCell.set('title', response.statusText);
                GM_log("error updating challenge header: " + response.statusText);
		callback({ success: false, msg: response.statusText } );
            },
            onSuccess: function (responseText, responseXML) {
                var tempDiv = new Element('div');
                tempDiv.innerHTML = responseText.stripScripts();
                var problem = tempDiv.getElement('p.Problem');
                if ($chk(problem)) {
                    callback({ success: false, msg: problem.innerHTML });
		} else {
			callback({ success: true });
		}
            }
        }).send();
    }

    function updateChallengeTitle(title, callback) {
        var buttonCell = $('UCPANG-promote-wizard-save-button');
        buttonCell.empty();
        buttonCell.adopt(spin = new Element('img', {
            src: updatingIcon
        }));
        // but first, get the edit link
	var challengeAnnouncement = $('DiscussTopic').getElement('td.Said');
        var editLink = challengeAnnouncement.getElement('small a[href*=edit]');
        if (!$chk(editLink)) {
	    callback({
	    	success: false,
		msg: 'error fetching edit link; aborting'
	    });
	    return;
        }
	var challengeAnnouncementHtml = stripChallengeAnnouncement(challengeAnnouncement);
	updateChallengeHeader(title, challengeAnnouncementHtml, buttonCell, editLink.href, callback);
    }

    function closeChallengeThread(callback) {
    	if (SIMULATE) {
		callback({ success: true });
		return;
	}
    	var lockHRefs = $$('a[href*=/lock/]');
	if (!$chk(lockHRefs) || lockHRefs.length == 0) {
		callback({ success: false, msg: 'unable to close thread: no \'lock\' link found' });
	} else {
		var lockHRef = lockHRefs[0].href;
		new Request({
			url: lockHRef,
			onSuccess: function (responseText, responseXML) {
                            var tempDiv = new Element('div');
                            tempDiv.innerHTML = responseText.stripScripts();
                            var problem = tempDiv.getElement('p.Problem');
                            if ($chk(problem)) {
                                callback({ success: false, msg: problem.innerHTML });
                            } else {
			    	var form = tempDiv.getElement('form[action*=lock]');
				new Request({
					async: false,
					url: form.action,
					method: form.method,
					data: {
						magic_cookie: form.getElement('input[name=magic_cookie]').value,
						done: 1,
						lock: 1
					},
					onSuccess: function (responseText, responseXML) {
		                            var tempDiv = new Element('div');
                		            tempDiv.innerHTML = responseText.stripScripts();
		                            var problem = tempDiv.getElement('p.Problem');
                		            if ($chk(problem)) {
                                		callback({ success: false, msg: problem.innerHTML });
		                            } else {
						callback({ success: true });
					    }
					},
					onFailure: function (response) {
			    			callback({ success: false, msg: response.statusText });
					}
				}).send();
			    }
			},
			onFailure: function (response) {
				callback({ success: false, msg: response.statusText });
			}
		}).post();
	}
    }

    function unstickyChallengeThread(callback) {
    	if (SIMULATE) {
		callback({ success: true });
		return;
	}
    	// var lockHRefs = $$('a[href*=/unstick/]'); has been removed when removing the 'small' elements
	var normalizedHref = /(.*flickr.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(document.location.href)[1];
	var lockHRef = normalizedHref + '/unstick/';
		new Request({
			url: lockHRef,
			onSuccess: function (responseText, responseXML) {
                            var tempDiv = new Element('div');
                            tempDiv.innerHTML = responseText.stripScripts();
                            var problem = tempDiv.getElement('p.Problem');
                            if ($chk(problem)) {
                                callback({ success: false, msg: problem.innerHTML });
                            } else {
			    	callback({ success: true });
			    }
			},
			onFailure: function (response) {
				callback({ success: false, msg: response.statusText });
			}
		}).post();
    }

    function stickifyChallengeThread(threadId, callback) {
    	if (SIMULATE) {
		callback({ success: true });
		return;
	}
	var normalizedHref = /(.*flickr.com\/groups\/[^\/]+\/discuss\/)\d+/.exec(document.location.href)[1];
	var lockHRef = normalizedHref + threadId + '/stick/';
		new Request({
			url: lockHRef,
			async: false,
			onSuccess: function (responseText, responseXML) {
                            var tempDiv = new Element('div');
                            tempDiv.innerHTML = responseText.stripScripts();
                            var problem = tempDiv.getElement('p.Problem');
                            if ($chk(problem)) {
                                callback( { success: false, error: problem.innerHTML });
                            } else {
			    	callback( { success: true });
			    }
			},
			onFailure: function (response) {
				callback( { success: false, error: response.statusText });
			}
		}).post();
    }

    function saveOverrides(challengeDefinitionUI, challengeAnnouncement, callback) {
        // empty buttonCell
        //buttonCell.style.backgroundColor = "white";
        var buttonCell = $('UCPANG:saveoverrides_button');
        buttonCell.empty();
        buttonCell.adopt(spin = new Element('img', {
            src: updatingIcon
        }));
        // read current content of announcement
        //var content = challengeAnnouncement.innerHTML;
        // remove all UCPoverrides, and <small> elements
        // but first, get the edit link
        var editLink = challengeAnnouncement.getElement('small a[href*=edit]');
        if (!$chk(editLink)) {
            buttonCell.empty();
            buttonCell.set('html', 'ERROR fetching edit link; aborting');
            buttonCell.style.color = 'red';
            return;
        }
        challengeAnnouncement.getElements('img[alt*=UCPoverride]').dispose();
	var challengeAnnouncementHtml = stripChallengeAnnouncement(challengeAnnouncement);
        // append the new overrides
        $('ucpng_overrides_title_panel').set('html', "checking overrides..");
        for (var key in challengeDefinitionUI.ui) {
            if (challengeDefinitionUI.ui.hasOwnProperty(key)) {
                if (!$chk(challengeDefinitionUI.ui[key])) {
                    continue;
                }
                var inputElement = challengeDefinitionUI.ui[key].element;
                var value = undefined; // reset each time
                switch (challengeDefinitionUI.ui[key].createInput.type) {
                case "numbers": // selectbox
                    value = parseInt(inputElement.value, 10); 
                    break;
                case "strings": // selectbox
                    value = inputElement.value;
                    break;
                case "boolean": // checkbox
                    value = inputElement.checked;
                    break;
                default:
                    messageSpan.set('html', "<br>input element for key '" + key + "' not found");
                }
                if (value !== challengeDefinitionUI.challengeDefinition.options[key]) {
                    challengeAnnouncementHtml += '<img ' +
		                                 'src="http://l.yimg.com/g/images/spaceout.gif" ' +
                                                 'alt="UCPoverride:' + challengeDefinitionUI.ui[key].overrideLabel + ':' + value + '" ' +
						 '/>';
                }
            }
        }
        // post: based on the ideas found in 'Inline Comment Editor' by steev 
        // (http://www.flickr.com/groups/flickrhacks/discuss/72157600231591623/)
        var title = challengeDefinitionUI.chlgname;
        var editLinkHref = editLink.href;

        $('ucpng_overrides_title_panel').set('html', "sending updates to Flickr, please wait..");
	updateChallengeHeader(title, challengeAnnouncementHtml, buttonCell, editLinkHref, callback);
    }

    var UCPChallengeDefinitionUI = new Class({
        initialize: function (challengeDefinition, chlgname) {
            this.challengeDefinition = challengeDefinition;
            this.chlgname = chlgname;
            this.ui = {
                neededPhotos:   {
                    label:          "photos",
                    overrideLabel:  "photos",
                    title:          "the number of photos needed in this challenge",
                    appendLabel:    "photos",
                    element:        undefined,
                    createInput:    {
                        type:   "numbers",
                        min:    -1,
                        max:    50
                    }
                },
                neededScore:    {
                    label:          "score",
                    overrideLabel:  "votes",
                    title:          "the score needed to call a winner",
                    appendLabel:    "votes wins",
                    element:        undefined,
                    createInput:    {
                        type:   "numbers",
                        min:    -1,
                        max:    50
                    }
                },
                scoreType:      {
                    label:          "voting",
                    overrideLabel:  "type",
                    title:          "the type of voting",
                    element:        undefined,
                    createInput:    {
                        type:   "strings",
                        values: [
                            [ "HORIZONTAL", "horizontal voting (1-1-0)" ],
                            [ "PIC-P-1", "pick player (only seen in MatchPoint)" ],
                            [ "PIC-V-1", "pick your favorite" ],
                            [ "PIC-V-2", "pick your 2 favorites (vertical voting)" ],
                            [ "PIC-V-3", "pick your 3 favorites (vertical voting)" ],
                            [ "PIC-V-4", "pick your 4 favorites (vertical voting)" ],
                            [ "PIC-V-5", "pick your 5 favorites (vertical voting)" ],
                            [ "PIC-H-2", "pick your 2 favorites (horizontal voting)" ],
                            [ "PIC-H-3", "pick your 3 favorites (horizontal voting)" ],
                            [ "PIC-H-4", "pick your 4 favorites (horizontal voting)" ],
                            [ "PIC-H-5", "pick your 5 favorites (horizontal voting)" ],
                            [ "RATE-PHOTO", "give points to some (or all) of the challengers" ],
                            [ "VERTICAL", "vertical voting" ],
                            [ "VERTICAL-WEIGHTED", "vertical voting, with weighted votes" ]
                        ]
                    }
                },
                countsToLimit:  {
                    label:          "group\nlimit",
                    overrideLabel:  "grouplimit",
                    title:          "indicates if playing in this challenge counts toward the limit",
                    element:        undefined,
                    createInput:    {
                        type:   "boolean",
                        title:  "counts towards the group limit"
                    }
                },
                limitPerPlayer: {
                    label:          "photos&nbsp;per\nchallenger",
                    overrideLabel:  "max",
                    title:          "maximum number of entries per player",
                    appendLabel:    "photos per challenger (max.)",
                    element:        undefined,
                    createInput:    {
                        type:   "numbers",
                        min:    -1,
                        max:    10
                    }
                },
                playerVoting: {
                    label:          "player&nbsp;voting",
                    overrideLabel:  "voting",
                    title:          "player participation",
                    element:        undefined,
                    createInput:    {
                        type:   "strings",
                        values: [
                            [ "n/a", "not applicable" ],
                            [ "mayvote", "a player in this challenge is allowed to vote in this challenge" ],
                            [ "mustvote", "a player in this challenge must also vote in this challenge" ],
                            [ "maynotvote", "a player in this challenge can not vote in this challenge" ]
                        ]
                    }
                },
                scoresAdded: {
                    label:          "incremental&nbsp;&nbsp;",
                    overrideLabel:  "added",
                    title:          "'Constructive' challenges should set this to 'false'",
                    element:        undefined,
                    createInput:    {
                        type:   "boolean"
                    }
                },
                iconChallenge: {
                    label:          "icon",
                    overrideLabel:  "icons",
                    title:          "Icon challenge?",
                    element:        undefined,
                    createInput: {
                        type:   "boolean"
                    }
                }
            }
        },
        printAsTableWithDiff: function (other, challengeAnnouncement, element, admin) {
            var table = new Element('table', {
                border: '0',
                cellSpacing: '0',
                cellPadding: '0'
            }).inject(element);
            table.adopt(new Element('tr').adopt(
                    new Element('th', { 
                        colSpan: 3,
                        html: "&nbsp;Challenge&nbsp;configuration"
                    }),
                    new Element('th', { colSpan: 5 }).adopt(
                        titlePanel = new Element('small', {
                            styles: {
                                color: "green"
                            },
                            id: "ucpng_overrides_title_panel",
                            html: " "
                        })
                    ),
                    new Element('th', { align: 'right' }).adopt(new Element('a', {
                        target: '_blank',
                        html: 'help',
                        href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623759768824/#comment72157623775144916',
                        styles: {
                            cursor: 'help'
                        }
                    }))
            ));
            table.adopt(row = new Element('tr', {
                    styles: {
                        background: '#CFCFCF'
                    },
                    cellPadding: 0 
                })
            );
            new Element('th', {
                html: " "
            }).inject(row);
            $each(this.ui, function (value, key) {
                    new Element('small', {
                        html: value.label,
                        events: {
                            'mouseover': function () {
                                titlePanel.set('html', value.title);
                            },
                            'mouseout': function () {
                                titlePanel.empty();
                            }
                        }
                    }).inject(new Element('th', { 
                                        styles: { 
                                            'border-left': 'solid white 1px',
                                            backgroundColor: value.debug ? 'orange' : undefined
                                        }
                                    }).inject(row));
                }
            );
            row = new Element('tr').inject(table);
            new Element('td', {
                html: "default:",
                styles: {
                    color: '#6F6F6F'
                },
                events: {
                    'mouseover': function () {
                        titlePanel.set('html', "the default values defined by UCheckPlay");
                    },
                    'mouseout': function () {
                        titlePanel.empty();
                    }
                }
            }).inject(row);
            var value;
            $each(this.ui, function (uiValue, key) {
                    value = this.challengeDefinition.options[key];
                    if (parseInt(value, 10) < 0 || parseInt(value, 10) === 65535) {
                        value = "n/a";
                    }
                    row.adopt(new Element('td', {
                        styles: {
                            backgroundColor: uiValue.debug ? 'orange' : undefined
                        }
                    }).adopt(new Element('small', {
                        html: value,
                        events: {
                            'mouseover': function () {
                                titlePanel.set('html', uiValue.title);
                            },
                            'mouseout': function () {
                                titlePanel.empty();
                            }
                        }
                    })));
                },
                this
            );
            if (this.challengeDefinition.nonChallengeType()) {
                return table;
            }
            overrideRow = new Element('tr').inject(table);
            var showOverrides = false; // only show override row when needed for non-admins/non-mods
            valueColumn = new Element('th', {
                html: 'override:',
                events: {
                    'mouseover': function () {
                        titlePanel.set('html', "overriden values are colored red");
                    },
                    'mouseout': function () {
                        titlePanel.empty();
                    }
                },
                valign: "top"
            }).inject(overrideRow);
            $each(this.ui, function (uiValue, key) {
                    var override = false;
                    var otherValue;
                    value = this.challengeDefinition.options[key];
                    if (other) {
                        otherValue = other.challengeDefinition.options[key];
                        override = otherValue !== value;
                        if (override) {
                            showOverrides = true;
                        }
                    }
                    if (parseInt(otherValue, 10) < 0 || parseInt(otherValue, 10) === 65535) {
                        value = "n/a";
                    }
                    valueColumn = new Element('td', {
                        styles: {
                            backgroundColor: uiValue.debug ? 'orange' : undefined
                        }
                    }).inject(overrideRow);
                    var small = new Element('small').inject(valueColumn);
                    switch (uiValue.createInput.type) {
                    case "numbers": 
                        var numbersSelect = new Element('select').inject(small);
                        uiValue.element = numbersSelect;
                        if (!admin) {
                            numbersSelect.disabled = 'true';
                        }
                        if (override) {
                            numbersSelect.style.color = "Red";
                        }
                        var min = uiValue.createInput.min;
                        var max = uiValue.createInput.max;
                        for (var n = min; n <= max; ++n) {
                            if (n === 0) {
                                continue;
                            }
                            var optionName = (n === -1 ? "n/a" : n);
                            var optionValue = n;
                            var numberOption = new Element('option', {
                                value: optionValue,
                                html: optionName,
                                selected: ((!override && n === this.challengeDefinition.options[key]) ||
                                 (override && n === other.challengeDefinition.options[key])) ?
                                    "selected" : "",
                                events: {
                                    'mouseover': function () {
                                        titlePanel.set('html', (parseInt(this.value, 10) === -1 ? 
                                                                "no limit" : this.value + " " + uiValue.appendLabel));
                                    },
                                    'mouseout': function () {
                                        titlePanel.empty();
                                    }
                                }
                            }).inject(numbersSelect);
                        }
                        break; 
                    case "strings":
                        var stringsSelect = new Element('select', {
                            styles: {
                                'max-width': 110
                            }
                        }).inject(small);
                        uiValue.element = stringsSelect;
                        if (!admin) {
                            stringsSelect.disabled = true;
                        }
                        if (override) {
                            stringsSelect.style.color = "Red";
                        }
                        $each(uiValue.createInput.values, function(stringArray) {
                            var optionName = stringArray[0];
                            var optionTitle = stringArray[1];
                            var stringOption = new Element('option', {
                                value: optionName,
                                html: optionName,
                                events: {
                                    'mouseover': function () {
                                        titlePanel.set('html', optionTitle);
                                    },
                                    'mouseout': function () {
                                        titlePanel.empty();
                                    },
                                },
                                selected: ((!override && optionName === this.challengeDefinition.options[key]) ||
                                 (override && optionName === other.challengeDefinition.options[key])) ?
                                    "selected": ""
                            }).inject(stringsSelect);
                        }, this);
                        break;
                    case "boolean":
                        if (override) {
                            var colorSpan = new Element('span').inject(small);
                            colorSpan.setAttribute("style", "background-color: red");
                        }
                        var checkBox = new Element('input', {
                            disabled: !admin,
                            type: 'checkbox',
                            name: key,
                            id: key,
                            title: uiValue.debug ? uiValue.title : undefined,
                            checked: ((!override && this.challengeDefinition.options[key]) || 
                                            (override && other.challengeDefinition.options[key])),
                            events: {
                                'mouseover': function () {
                                    titlePanel.set('html', uiValue.title);
                                },
                                'mouseout': function () {
                                    titlePanel.empty();
                                }
                            }
                        }).inject(override ? colorSpan : small);
                        uiValue.element = checkBox;
                        break;
                    }
                },
                this
            );
            if (showOverrides || admin) {
                overrideRow.inject(table);
            }
            if (admin) {
                (buttonCell = new Element('button', {
                    'class': "Butt",
                    html: 'SAVE',
                    id: "UCPANG:saveoverrides_button",
                    events: {
                        'click': function () { 
                            saveOverrides(this, challengeAnnouncement, function (retval) {
                               if (retval.success) {
                                       if (!SIMULATE) document.location.href = /(.*flickr.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(document.location.href)[1] + '/';
                               } else {
                                       alert(retval.msg);
                               }
                           });
                        }.bind(this),
                        'mouseover': function () {
                            titlePanel.set('html', "Store overrides in challenge announcement");
                        },
                        'mouseout': function () {
                            titlePanel.empty();
                        }
                    }
                })).inject(new Element('td', {
                    colSpan: 20,
                    align: "right"
                }).inject(new Element('tr').inject(table)));
            }
            return table;
        }
    });


    function cleanupUCPAvariables() {
        var keyValues = GM_listValues();
        for (var keyIdx = 0, valLen = keyValues.length; keyIdx < valLen; ++keyIdx) {
            var key = keyValues[keyIdx];
            //GM_log("found key: " + key);
            if (//key.match(/UCPA/) &&
                    !key.match(/UCPA.hideNonWonMedals/) &&
                    !key.match(/UCPA.stopAtFirstFoundMedal/) &&
                    !key.match(/UCPA.defaultAwardsGroup/) &&
                    !key.match(/UCPA.defaultAwardsGroupInPendingItems/) &&
                    !key.match(/UCPA.bumpAndReload/) &&
                    !key.match(/UCPA.bumpAndReturnToFirstPage/) &&
                    !key.match(/UCPA.smartPhotoBoxes/) &&
		    !key.match(/UCPA.workflowAutoClaimOwnership/) &&
		    !key.match(/UCPA.useLevenshteinToMarkUsedThemes/) &&
		    !key.match(/UCPA.levenshteinDistanceInUsedThemes/) &&
                    !key.match(/UCPA.lastVersionCheckTime/) &&
		    !key.match(/UCPA.CrossCheck/)) {
                GM_deleteValue(key);
            }
        }
    }

    var bumpingTopics = [];
    var bumpingErrors = false;
    function cleanupBumpCheckBoxes(checkBox, bumpingTopics, success, showFeedback, feedbackMessage, 
                bumpButton, finalizeCallback) {
        if (showFeedback) {
            var feedbackImg = new Element('img', {
                src: success ? "http://l.yimg.com/g/images/icon_check_small.png" : 
                               errorIcon,
                height: "12",
                title: $chk(feedbackMessage) ? feedbackMessage : ""
            }).inject(checkBox, 'before');
        }
        if (!success) {
            bumpingErrors = true;
        }
        var otherBumpFound = false;
        for (var bumpIdx = 0, bumpLen = bumpingTopics.length; bumpIdx < bumpLen; ++bumpIdx) {
            var storedBumpTopic = bumpingTopics[bumpIdx];
            if (storedBumpTopic !== null) {
                if (storedBumpTopic.name === checkBox.name) {
                    bumpingTopics[bumpIdx] = null;
                } else {
                    otherBumpFound = true;
                }
            }
        }
        if (otherBumpFound) {
            return;
        } else {
            bumpButton.getElement('img').set('src', images.bump);
            bumpButton.set('disabled', false);
            finalizeCallback();
        }
    }

    function deleteThreadReply(data) {
    	var deleteCommentUrl = data.replyPermalink + "/delete";
	var async = data.async;
	var callback = data.callback;
	var referrer = data.referrer;

        new Request({
            url: deleteCommentUrl,
	    async: async,
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Referer": referrer
	    },
            onFailure: function (response) {
	    	callback({ success: false, msg: response.statusText });
            },
            onSuccess: function (responseText, responseXML) {
	        var tempDiv = new Element('div');
        	tempDiv.innerHTML = responseText.stripScripts();
                var problem = tempDiv.getElement('p.Problem');
                if ($chk(problem)) {
                    callback({ success: false, msg:  problem.innerHTML });
	        } else {
		    callback({ success: true });
		}
	    }
        }).post({
            magic_cookie: GM_getAuthHash(),
            done: 1
	});
    }

    function postThreadReply(data) {
    	var threadUrl = data.threadUrl;
	var message = data.message;
	var callback = data.callback;
	var async = data.async;

        new Request({
            url: threadUrl + "#reply",
	    async: async,
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Referer": ucpThread.url()
            },
            onFailure: function (response) {
	    	callback({ success: false, msg: response.statusText });
            },
            onSuccess: function (responseText, responseXML) {
	        var tempDiv = new Element('div');
        	tempDiv.innerHTML = responseText.stripScripts();
                var problem = tempDiv.getElement('p.Problem');
                if ($chk(problem)) {
                    callback({ success: false, msg:  problem.innerHTML });
	        } else {
		    callback({ success: true });
		}
	    }
        }).post({
	    magic_cookie: GM_getAuthHash(),
                    done: 1,
                 message: message
	    }
	);
    }

    function updateThreadReply(data) {
	var async = data.async;
	var editLink = data.replyUrl + '/edit/';
	var message = data.message;
	var callback = data.callback;
        // strip the starting 'p' tag
        message = message.replace(/^\s*<p[^>]*>/,'');
        new Request({
            method: "post",
            url: editLink,
	    async: async,
            headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Referer": editLink
            },
            data: "magic_cookie=" + GM_getAuthHash() + 
                                "&done=1" +
                                "&message=" + encodeURIComponent(message),
            onFailure: function (response) {
		callback({ success: false, msg: response.statusText } );
            },
            onSuccess: function (responseText, responseXML) {
                var tempDiv = new Element('div');
                tempDiv.innerHTML = responseText.stripScripts();
                var problem = tempDiv.getElement('p.Problem');
                if ($chk(problem)) {
                    callback({ success: false, msg: problem.innerHTML });
		} else {
		    callback({ success: true });
		}
            }
        }).send();
    }

    function bumpChallenge(ucpThread, bumpButton) {
        var url = /.*flickr.com\/groups\/[^\/]+\/discuss\/\d+/.exec(ucpThread.url());
        bumpButton.getElement('img').set('src', updatingIcon);
        bumpButton.set('title', 'bumping..');
	postThreadReply({
		threadUrl: url,
		message: groupConfig.bumpMessage() + '<img src=http://l.yimg.com/g/images/spaceout.gif alt=UCPANG:bump:'+new Date().getTime()+'/>',
		callback: function(result) {
			if (result.success == false) {
	      		    try {
                		GM_log("error bumping '" + ucpThread.topic() + "': " + result.msg);
                		bumpButton.getElement('img').set('src', errorIcon);
                		bumpButton.set('title', "error while posting bump message: " + result.msg);
	      		    } catch (e) {
	        		GM_log("error bumping: " + e);
	      		    }
			} else {
	        		var bumps = new Array();
				var tempDiv;
	        		try {
                		// if it fails, it will show the Discuss page
                		// try to guess:
                		// the bump msg(s) will most probably be at the last page of the thread
                    		    var lastPage = url + "/lastpage";
                    		    new Request({
                        		url: lastPage,
                        		async: false,
                        		onFailure: function (response) {
                            			bumpButton.getElement('img').set('src', errorIcon);
                            			bumpButton.set('title', "error while deleting (failed to get lastpage): " + response.statusText);
                        		},
                        		onSuccess: function (responseText, responseXML) {
                            			tempDiv = new Element('div');
                            			tempDiv.innerHTML = responseText.stripScripts();
                            			var problem = tempDiv.getElement('p.Problem');
                            			if ($chk(problem)) {
                                			bumpButton.getElement('img').set('src', errorIcon);
                                			bumpButton.set('title', "error while deleting: " + problem.innerHTML);
                                			return;
                            			}
                            			var bumpMessages = document.evaluate(
                                			".//table[@class='TopicReply']//td[@class='Said']//img[contains(@alt,'UCPANG:bump')]//parent::p//parent::td",
                                			tempDiv,
                                			null,
                                			XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
                                			null);
			    			var messages = document.evaluate(
			        			".//table[@class='TopicReply']//td[@class='Said']",
			    				tempDiv,
							null,
							XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
							null);
			    			var lastMessage =  messages.snapshotItem(messages.snapshotLength - 1);
			    			var lastMessageId = document.evaluate(".//parent::tr//td[contains(@class,'Who')]//a[contains(@name,'comment')]",
			    				lastMessage,
							null,
							XPathResult.FIRST_ORDERED_NODE_TYPE,
							null).singleNodeValue.name
                            			for (var i = 0, len = bumpMessages.snapshotLength; i < len; ++i) {
                                			var bumpMessage = bumpMessages.snapshotItem(i);
							var messageId = document.evaluate(".//parent::tr//td[contains(@class,'Who')]//a[contains(@name,'comment')]",
				    				bumpMessage,
				    				null,
				    				XPathResult.FIRST_ORDERED_NODE_TYPE,
				    				null).singleNodeValue.name;
                                			if (messageId != lastMessageId) {
								bumps[bumps.length] = bumpMessage;
                                			}
                            			}
                        		}
                    		    }).get();
                    			if (bumps.length == 0) {
                        			bumpButton.getElement('img').set('src', defaultCheckIconSmall);
                        			bumpButton.set('title', "bumped (no comments deleted)");
                    			} else {
		        			bumps.each(function(bumpMessage) {
	                    			// if on multiple pages, the page stitching results in a post to page2
        	            			// deleting with page2 in the url does not work
			    				var messageId = document.evaluate(".//parent::tr//td[contains(@class,'Who')]//a",
			    					bumpMessage,
								null,
								XPathResult.FIRST_ORDERED_NODE_TYPE,
								null).singleNodeValue.name;
                	    				var commentUrl = url + '/' + messageId.replace('comment', '');
			    				deleteThreadReply( {
			    					replyPermalink: commentUrl,
								referrer: ucpThread.url(),
								async: true,
								callback: function (data) {
									bumpButton.getElement('img').set('src', data.success == false ? errorIcon : defaultCheckIconSmall);
	                            					bumpButton.set('title', data.success == false ? 
										"error while deleting comment (" + data.msg + ")" : "bumped successfully");
								}
			    				});
                        			});
		    			}
				} catch (e) {
		    			GM_log("error bumping2: " + e);
				}
			}
		}
	});

    }

    function bumpTopic(checkBox, bumpingTopics, bumpButton, finalizeCallback) {
        var topic = checkBox.name;
        new Request({
            url: topic + "#reply",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Referer": topic
            },
            onFailure: function (response) {
                GM_log("error bumping '" + topic + "': " + response.statusText);
                cleanupBumpCheckBoxes(checkBox, bumpingTopics, false, true, "error posting bump (" + response.statusText + ")", bumpButton, finalizeCallback);
            },
            onSuccess: function (responseText, responseXML) {
	        var bumps = new Array();
                var tempDiv = new Element('div');
                tempDiv.innerHTML = responseText.stripScripts();
                var problem = tempDiv.getElement('p.Problem');
                if ($chk(problem)) {
                    cleanupBumpCheckBoxes(checkBox, 
                        bumpingTopics, 
                        false, 
                        true, 
                        "error: " + problem.get('html'), 
                        bumpButton, 
                        finalizeCallback);
                    return;
                }
                // try to guess:
                // the bump msg(s) will most probably be at the last page of the thread
                var lastPage = topic + "/lastpage";
                new Request({
                    url: lastPage,
                    async: false,
                    onFailure: function (response) {
                        cleanupBumpCheckBoxes(checkBox,
                            bumpingTopics,
                            false,
                            true,
                            "error while deleting (failed to get lastpage): " + response.statusText,
                            bumpButton,
                            finalizeCallback);
                     },
                     onSuccess: function (responseText, responseXML) {
                         var innerTempDiv = new Element('div');
                         innerTempDiv.innerHTML = responseText.stripScripts();
                         var problem = innerTempDiv.getElement('p.Problem');
                         if ($chk(problem)) {
                             cleanupBumpCheckBoxes(checkBox,
                                 bumpingTopics,
                                 false,
                                 true,
                                 "error while deleting: " + problem.innerHTML,
                                 bumpButton,
                                 finalizeCallback);
                             return;
                         }
                         var bumpMessages = document.evaluate(
                             ".//table[@class='TopicReply']//td[@class='Said']//img[contains(@alt,'UCPANG:bump')]//parent::p//parent::td",
                             innerTempDiv,
                             null,
                             XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
                             null);
			 var messages = document.evaluate(".//table[@class='TopicReply']//td[@class='Said']",
			     innerTempDiv,
		             null,
			     XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
			     null);
			 var lastMessage =  messages.snapshotItem(messages.snapshotLength - 1);
			 var lastMessageId = document.evaluate(".//parent::tr//td[contains(@class,'Who')]//a[contains(@name,'comment')]",
			    	lastMessage,
				null,
				XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue.name
                         for (var i = 0, len = bumpMessages.snapshotLength; i < len; ++i) {
                             var bumpMessage = bumpMessages.snapshotItem(i);
			     var messageId = document.evaluate(".//parent::tr//td[contains(@class,'Who')]//a[contains(@name,'comment')]",
				    bumpMessage,
				    null,
				    XPathResult.FIRST_ORDERED_NODE_TYPE,
				    null).singleNodeValue.name;
			     if (messageId != lastMessageId) {
			         bumps[bumps.length] = bumpMessage;
			    }
                        }
                    }
                }).get();
                if (bumps.length == 0) {
                    cleanupBumpCheckBoxes(checkBox,
                        bumpingTopics,
                        true,
                        true,
                        "bumped (reload page to see result)",
                        bumpButton,
                        finalizeCallback);
                } else {
                    // the normal, easy way
		    bumps.each(function (bumpMessage) {
		      try {
                        // if on multiple pages, the page stitching results in a post to page2
                        // deleting with page2 in the url does not work
			var messageId = document.evaluate(".//parent::tr//td[contains(@class,'Who')]//a",
			    	bumpMessage,
				null,
				XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue.name;
                	var commentUrl = topic + '/' + messageId.replace('comment', '');
			deleteThreadReply( {
			    	replyPermalink: commentUrl,
				referrer: topic,
				async: true,
				callback: function (data) {
				    if (data.success == false) {
                                	cleanupBumpCheckBoxes(checkBox, bumpingTopics, false, true, "error posting delete: " + response.statusText, bumpButton, finalizeCallback);
				    } else {
                                    	cleanupBumpCheckBoxes(checkBox, bumpingTopics, true, true, "bumped (reload page to see result)", bumpButton, finalizeCallback);
				    }
				}
			});
		      } catch (e) {
		          GM_log("error: " + e);
		      }
		    });
                }
            }
        }).post({
            magic_cookie: GM_getAuthHash(),
                    done: 1,
                 message: groupConfig.bumpMessage() + '<img src=http://l.yimg.com/g/images/spaceout.gif alt=UCPANG:bump:'+new Date().getTime()+'/>'
	});
    }

    function startBumping(checkBoxes, bumpButton) {
        $each(checkBoxes, function (checkBox) {
            bumpTopic(checkBox, checkBoxes, bumpButton, function () {
                if (groupPreferences.bumpAndReload() && !bumpingErrors) {
                    if (groupPreferences.bumpAndReturnToFirstPage()) {
                        document.location.href = 'http://www.flickr.com/groups/' + groupPreferences.groupConfig().groupname() + '/discuss/';
                    } else {
                        document.location.href = document.location.href;
                    }
                }
            });
        });
    }

    function makeSticky(allCheckBoxes, stickyButton) {
	nThreadsToMakeStick = 0;
        var selectedCheckBoxes = [];
        allCheckBoxes.each(function (checkBox) {
            if (checkBox.checked) {
                selectedCheckBoxes.push(checkBox);
            }
        });
	if (selectedCheckBoxes.length > 0) {
		stickyButton.getElement('img').set('src', updatingIcon);
	}
	nThreadsToMakeStick = selectedCheckBoxes.length;
        selectedCheckBoxes.each(function (checkBox) {
		var stickyImg = $('UCPANG-stickify-thread-' + checkBox.name);
		if (!$chk(stickyImg)) {
			stickyImg = new Element('img', {
				src: updatingIcon,
				title: 'making sticky'
			}).inject(checkBox, 'after');
		}
		var lockHRef = checkBox.name + '/stick/';
		new Request({
			url: lockHRef,
			onSuccess: function (responseText, responseXML) {
			    --nThreadsToMakeStick;
                            var tempDiv = new Element('div');
                            tempDiv.innerHTML = responseText.stripScripts();
                            var problem = tempDiv.getElement('p.Problem');
                            if ($chk(problem)) {
				stickyImg.set('src', errorIcon);
				stickyImg.set('title', "error while making thread sticky: " + problem.innerHTML);
                            } else {
			    	stickyImg.set('src', defaultCheckIconSmall);
				stickyImg.set('title', "made sticky");
			    }
			    if (nThreadsToMakeStick == 0) { // done
				stickyButton.getElement('img').set('src', images.sticky);
			    }
			},
			onFailure: function (response) {
			    --nThreadsToMakeStick;
			    stickyImg.set('src', defaultCheckIconSmall);
			    stickyImg.set('title', response.statusText);
			    if (nThreadsToMakeStick == 0) { // done
				stickyButton.getElement('img').set('src', images.sticky);
			    }
			}
		}).post();
	});
    }

    function startExpanding(checkBoxes) {
        checkBoxes.each(function (checkBox) {
            if (checkBox.checked) {
                var id = 'UCPANG.arrow.' + checkBox.name;
                $(id).click();
            }
        });
    }
    function startCrossInspect(checkBoxes) {
	/*
	for each selectedThread
		read selectedThread
		collect players, with playtime
		collect voters, with votetime
		guess start of voting
	report
		for each player, number of entered challenges
		for each entry, show if player has voted in VOTE challenges
	*/

	if ($chk($('UCPANG-cross-inspect-dialog'))) {
		$('UCPANG-cross-inspect-dialog').getParent('table').destroy();
		return;
	}
        var crossDialog = new Element('td', {
		id: 'UCPANG-cross-inspect-dialog'
	}).inject(new Element('tr').inject(new Element('table', { // put into a table to get the CSS styles right
		border: 0,
		width: '100%',
                styles: ucpDialogStyle
	}).inject(topicListingTable, 'before')));
	var readingProgressRow = new Element('div').inject(crossDialog);
	var readingProgressLabel = new Element('span', { html: 'reading threads: ', styles: { fontWeight: 'bold' } }).inject(readingProgressRow);
	var collectingProgressRow = new Element('div').inject(crossDialog);
	var collectingProgressLabel = new Element('span', { html: 'collecting data: ', styles: { fontWeight: 'bold' } }).inject(collectingProgressRow);
	var availableDiv = new Element('div', { html: 'Available Cross-Inspection checks: ', styles: { fontWeight: 'bold' } }).inject(crossDialog);
	new Element('img', {
		src: images.info,
		styles: {
			cursor: 'pointer',
			height: '14px'
		}
	}).inject(new Element('a', {
		href: 'http://www.flickr.com/groups/unified_checkplay/discuss/72157623759768824/#comment72157629450524932',
		target: '_blank',
		title: 'click to get more information',
		styles: {
			backgroundColor: 'transparent'
		}
	}).inject(availableDiv));
	var labelsTable = new Element('table').inject(crossDialog);
	var checkLabels = {
		numbering:      { label: groupConfig.challengeNumberingLabel(), value: groupPreferences.crossCheckChallengeNumbering(), setF: 'setCrossCheckChallengeNumbering' },
		similarThemes:  { label: 'check for similar themes',            value: groupPreferences.crossCheckSimilarThemes(), setF: 'setCrossCheckSimilarThemes' },
		playerEntries:  { label: 'count player entries',                value: groupPreferences.crossCheckCountPlayerEntries(), setF: 'setCrossCheckCountPlayerEntries' },
		multiChallenge: { label: 'check for photos that participate in multiple challenges',     value: groupPreferences.crossCheckMultipleChallenges(), setF: 'setCrossCheckMultipleChallenges' },
		nonVoters:      { label: 'check for players that didn\'t vote (yet) in VOTE challenges', value: groupPreferences.crossCheckNonVoters(), setF: 'setCrossCheckNonVoters' }
	};
	for (var check in checkLabels) {
		if (checkLabels.hasOwnProperty(check)) {
			var labelRow = new Element('tr').inject(labelsTable);
			new Element('td', { html: '&emsp;' }).inject(labelRow);
			new Element('td', { html: '&bull; ' + checkLabels[check].label }).inject(labelRow);
			var runTypeCell = new Element('td').inject(labelRow);
			var runTypeSelect = new Element('select', {
				id: 'UCPANG-CrossCheck-runtype-' + check,
				events: {
					change: function (evt) {
						var check = this.get('id').replace('UCPANG-CrossCheck-runtype-', '');
						groupPreferences[checkLabels[check].setF].apply(groupPreferences,this.getSelected().get('value'));
					}
				}
			}).inject(runTypeCell);
			[ { html: 'always run this check', value: 'always' },
			  { html: 'never run this check', value: 'never' },
			  { html: 'default: only run this check in auto-mode', value: 'default' }
			].each(function (option) {
				new Element('option', {
					html: option.html,
					value: option.value,
					selected: (option.value == checkLabels[check].value ? "selected" : "")
				}).inject(runTypeSelect);
			});
		}
	};

	var readingThreads = [];
	var ucpThreads = [];
	// if no checkbox is checked, pretend they all are
	var noThreadsSelected = !checkBoxes.some(function (checkbox) { return checkbox.checked; });
	checkBoxes.each(function (checkBox) { 
		if (checkBox.checked || noThreadsSelected) { 
			readingThreads.push(true);
			new Element('span', { html: ' ' }).inject(readingProgressRow);
			new Element('img', { src: images.queued, id: 'UCPANG-cross-inspect-read-' + checkBox.name }).inject(readingProgressRow);
			new Element('span', { html: ' ' }).inject(collectingProgressRow);
			new Element('img', { src: images.queued, id: 'UCPANG-cross-inspect-collect-' + checkBox.name }).inject(collectingProgressRow);
		}
	});
	// collect data
        var topicListingHeaderRow = topicListingTable.getElement('tr');
        var headerColumns = topicListingHeaderRow.getElements('th');
        var challengeColumnIdx = 0;
        var challengeColumnHeader = headerColumns[0];
        if (challengeColumnHeader.textContent.match('UCP')) {
            challengeColumnIdx = 1;
            challengeColumnHeader = headerColumns[1];
        }
        checkBoxes.each(function (checkBox) {
            if (checkBox.checked || noThreadsSelected) {
		$('UCPANG-cross-inspect-read-' + checkBox.name).set('src', updatingIcon);
                var challengeUrl = checkBox.name;
		var challengeName = checkBox.getParent('tr').getElements('a')[challengeColumnIdx].textContent;
            	var ucpThread = ucpCreateChallengeThread({
                	groupConfig: groupConfig,
                	url: challengeUrl,
			chlgname: challengeName/*,
                	replyDecoratorFactory: ucpReplyDecoratorFactory,
                	decoratorFactory: ucpDecoratorFactory*/
            	});
		ucpThreads.push(ucpThread);

		if (groupConfig.skipChallenge(ucpThread.challengeName()) || !(ucpThread instanceof UCPChallengeThread)) {
			$('UCPANG-cross-inspect-read-' + ucpThread.url()).set('src', defaultCheckIconSmall);
			$('UCPANG-cross-inspect-read-' + ucpThread.url()).set('title', 'challenge thread \'' + ucpThread.challengeName() + '\' read');
			// no need to collect anything
			$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('src', defaultCheckIconSmall);
			$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('title', 'no need to collect data from \'' + ucpThread.challengeName() + '\'');
			readingThreads.pop();
			if (readingThreads.length == 0) {
				performCrossChecks(ucpThreads, failedRemoteActions, noThreadsSelected, checkLabels);
			}
		} else {
            	    ucpThread.loadthread(groupPreferences, function(result) {
			var success = result.success;
			var msg = result.msg;
			var discussionTopic = result.discussionTopic;
			var ucpThread = result.ucpThread;
			if (!success) {
				GM_log("ERROR: " + msg);
				failedRemoteActions.push(true);
				$('UCPANG-cross-inspect-read-' + ucpThread.url()).set('src', errorIcon);
				$('UCPANG-cross-inspect-read-' + ucpThread.url()).set('title', msg + ' (' + ucpThread.challengeName() + ')');
				readingThreads.pop();
				if (readingThreads.length == 0) {
					performCrossChecks(ucpThreads, failedRemoteActions, noThreadsSelected, checkLabels);
				}
			} else {
				$('UCPANG-cross-inspect-read-' + ucpThread.url()).set('src', defaultCheckIconSmall);
				$('UCPANG-cross-inspect-read-' + ucpThread.url()).set('title', 'challenge thread \'' + ucpThread.challengeName() + '\' read');
				// collect photos, and votes
				$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('src', updatingIcon);
				$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('title', 'collecting data from \'' + ucpThread.challengeName() + '\'');
				// TODO
        			try {
			            var challengeAnnouncementCell = $(discussionTopic).getElement('td.Said');
        			} catch (e) {
			            GM_log("error: " + e);
        			    // ignore
		        	}
	        		if (!$chk(challengeAnnouncementCell) || challengeAnnouncementCell.lenght === 0) {
		        	    GM_log("no announcement");
				    failedRemoteActions.push(true);
        	    		    $('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('src', errorIcon);
				    $('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('title', '\'' + ucpThread.challengeName() + '\': no announcement found!?');
				    readingThreads.pop();
				    if (readingThreads.length == 0) {
					performCrossChecks(ucpThreads, failedRemoteActions, noThreadsSelected, checkLabels);
				    }
        			} else {
					ucpThread.setChallengeAnnouncementNode(challengeAnnouncementCell);
					if (ucpThread.photos().length == 0) {
					    try {
						var challengeEntries = $(discussionTopic).getElements('td.Said');
    						ucpThread.challengeDefinition().readChallengeDefinitionOverrides(challengeAnnouncementCell);
        					ucpThread.collectVotes(challengeAnnouncementCell, challengeEntries);
					    } catch (e) {
						failedRemoteActions.push(true);
						$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('src', errorIcon);
						$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('title', e + ' (' + ucpThread.challengeName() + ')');
						readingThreads.pop();
						if (readingThreads.length == 0) {
							performCrossChecks(ucpThreads, failedRemoteActions, noThreadsSelected, checkLabels);
						}
						return;
					    }
					}
					$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('src', defaultCheckIconSmall);
					$('UCPANG-cross-inspect-collect-' + ucpThread.url()).set('title', 'data collected for \'' + ucpThread.challengeName() + '\'');
					readingThreads.pop();
					if (readingThreads.length == 0) {
						performCrossChecks(ucpThreads, failedRemoteActions, noThreadsSelected, checkLabels);
					}
				}
			}
	    	    });
		}
	    }
    	});
    }

    function performCrossChecks(ucpThreads, failedActions, automode, checkLabels) {
	var crossInspectDialog = $('UCPANG-cross-inspect-dialog');
	if (failedActions.length > 0) {
		new Element('div', { html: 'errors have occurred - aborting', styles: { fontWeight: 'bold', color: 'red' } }).inject(crossInspectDialog);
		new Element('div', { html: '(you could try again with the offending challenge removed from your selection)' }).inject(crossInspectDialog);
		return;
	}
	// challenge numbering
	new Element('img', {
		src: images.down,
		id: 'UCPANG-cross-inspect-challenge-numbering',
		title: 'click to ' + checkLabels.numbering.label + ', using RegExp ',
		alt: checkLabels.numbering.value,
		'class': 'UCPANG-collapsed',
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				var detail = $('UCPANG-cross-inspect-challenge-numbering-detail');
				if (this.hasClass('UCPANG-collapsed')) {
					this.set('class', 'UCPANG-expanded');
					this.set('src', images.up);
					var regExp = $('UCPANG-cross-inspect-challenge-numbering-regexp').value;
					detail.setStyles( { visibility: 'visible', display: 'block' });
					if (!$chk(regExp)) {
						new Element('label', {
							html: 'can not check without a regular expression',
							styles: {
								color: 'red'
							}
						}).inject(detail);
						return;
					}
					var numberingRegExp = new RegExp(regExp);
					if (!$chk(numberingRegExp)) {
						new Element('label', {
							html: 'invalid regular expression',
							styles: {
								color: 'red'
							}
						}).inject(detail);
						return;
					}
					var list = new Element('ul').inject(detail);
					var lastNumber = -1;
					var missingNumbers = [];
					var irrelevantThreads = [];
					var nonChallengeThreads = [];
					var invalidFormatThreads = [];
					var invalidStateThreads = [];
					ucpThreads.filter(function (ucpThread) {
						if (ucpThread instanceof UCPNonChallengeThread) {
							ucpThread.resetStatus();
							nonChallengeThreads.push(ucpThread);
							return false;
						}
						if (ucpThread instanceof UCPChallengeThread) {
							ucpThread.checkStatus();
							switch(ucpThread.challengeStatus()) {
        							case "UPDATING":
            							case "none":
            							case "Unknown":
									invalidStateThreads.push(ucpThread);
									return false;

								case "Closed":
								case "Voided":
								case "Expired":
									irrelevantThreads.push(ucpThread);
									return false;
    
								case "Open":
								case "Filled":
								case "Finished":
								case "--VOTE--":
									if (!ucpThread.challengeName().test(numberingRegExp)) {
										invalidFormatThreads.push(ucpThread);
										return false;
									}
									return true;
							}	
						} else { // instanceof UCPUnknownThread
							invalidFormatThreads.push(ucpThread);
							return false;
						}
						return ucpThread.challengeName().test(numberingRegExp);
					}).sort(function (thread1, thread2) {
						var name1 = thread1.challengeName();
						var name2 = thread2.challengeName();
						var number1 = numberingRegExp.exec(name1)[1];
						var number2 = numberingRegExp.exec(name2)[1];
						if (!isNaN(number1) && !isNaN(number2)) {
							try {
								return parseInt(number1, 10) - parseInt(number2, 10);
							} catch (e) {
								// ignore: sort alphabetically
							}
						}
						return name1 < name2 ? -1 : (name2 < name1 ? 1 : 0);
					}).each(function (ucpThread) {
						var color = '';
						var challengeNumber = parseInt(numberingRegExp.exec(ucpThread.challengeName())[1], 10);
						ucpThread.checkStatus();
						if (lastNumber == -1) { // first one to process
							lastNumber = challengeNumber - 1; // begin with the lowest
						}
						if (challengeNumber <= lastNumber) color = 'red';
						if (challengeNumber > lastNumber + 1) { // numbers missing ?
							if (challengeNumber == lastNumber + 2) { missingNumbers.push(lastNumber + 1); }
							else { missingNumbers.push((lastNumber + 1) + '-' + (challengeNumber - 1)); }
						}
						new Element('a', {
							href: ucpThread.url(),
							html: ucpThread.challengeName(),
							target: '_blank',
							styles: {
								color: color
							}
						}).inject(new Element('li').inject(list));
						lastNumber = challengeNumber;
					});
					if (missingNumbers.length > 0) {
						new Element('div', { 
							html: '&emsp;possibly missing: ' + missingNumbers, 
							styles: { 
								color: 'red',
								fontWeight: 'bold'
							}
						}).inject(detail);
						new Element('p').inject(detail);
					}

					[ { threads: invalidFormatThreads, 
						title: 'non-matching threads:' , color: 'red', postfix: '' },
					  { threads: nonChallengeThreads , 
						title: 'non-challenge threads (chat, showroom, ..):', color: '',    postfix: 'status' },
					  { threads: invalidStateThreads , 
						title: 'invalid states:',        color: 'red', postfix: 'status' },
					  { threads: irrelevantThreads   , 
						title: 'irrelevant threads (closed, voided, ..):',    color: '',    postfix: 'status' }]
					  .each(function (nmt) {
						var threads = nmt.threads;
						if (threads.length > 0) {
							var div = new Element('div').inject(detail);
							var label = new Element('label', {
								html: '&emsp;found ' + nmt.threads.length + ' ' + nmt.title,
								styles: {
									fontWeight: nmt.color != '' ? 'bold' : '',
									color: nmt.color,
									cursor: 'pointer'
								},
								events: {
									click: function (evt) {
										if ($chk(this.getParent('div').getElement('ul'))) {
											this.getParent('div').getElement('ul').destroy();
											return;
										}
										var list = new Element('ul').inject(this.getParent('div'));
										threads.sort(function (a,b) {
											var nameA = a.challengeName().toLowerCase( );
											var nameB = b.challengeName().toLowerCase( );
											if (nameA < nameB) {return -1}
											if (nameA > nameB) {return 1}
											return 0;
										}).each(function (ucpThread) {
											var item = new Element('li').inject(list);
											new Element('a', {
												href: ucpThread.url(),
												html: ucpThread.challengeName(),
												target: '_blank',
												styles: {
													fontWeight: 'normal',
													color: nmt.color
												}
											}).inject(item);
											if (nmt.postfix == 'status') {
												new Element('label', { html: ' == ' + ucpThread.challengeStatus() }).inject(item);
											}
										});
									}
								}
							}).inject(div);
							if (nmt.color != '') {
								label.fireEvent('click');
							}
						}
					});
				} else {
					this.set('class', 'UCPANG-collapsed');
					this.set('src', images.down);
					detail.empty();
					detail.setStyles({ visibility: 'hidden', display: 'none' });
				}
			}
		}
	}).inject(crossInspectDialog);
	new Element('label', {
		id: 'UCPANG-cross-inspect-challenge-numbering-label',
		html: ' ' + checkLabels.numbering.label + ' ',
		title: 'click to ' + checkLabels.numbering.label,
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				$('UCPANG-cross-inspect-challenge-numbering').fireEvent('click');
			}
		}
	}).inject(crossInspectDialog);
	new Element('input', {
		id: 'UCPANG-cross-inspect-challenge-numbering-regexp',
		value: groupConfig.challengeNumberingRegExp(),
		disabled: true,
		'class': 'UCPANG-disabled'
	}).inject(crossInspectDialog);
	new Element('span', { html: ' ' }).inject(crossInspectDialog);
	new Element('img', {
		src: images.edit,
		title: 'click to edit regular expression',
		styles: {
			cursor: 'pointer',
			height: '14px'
		},
		events: {
			click: function (evt) {
				var input = $('UCPANG-cross-inspect-challenge-numbering-regexp');
				var toEditmode = input.hasClass('UCPANG-disabled');
				input.set('class', toEditmode ? 'UCPANG-enabled' : 'UCPANG-disabled');
				input.set('disabled', !toEditmode);
				this.set('src', toEditmode ? images.save : images.edit);
				this.set('title', 'click ' + ( toEditmode ? 'to save' : 'to edit' ) + ' this regular expression');
				// save?
				if (!toEditmode) { // clicked save
					GM_setValue('UCPA.CrossCheckChallengeNumberingRegExp.' + groupConfig.groupname(), input.value);
					if (input.value == groupConfig.options.challengeNumberingRegExp || input.value.length == 0) {
						GM_deleteValue('UCPA.CrossCheckChallengeNumberingRegExp');
					}
					$('UCPANG-cross-inspect-challenge-numbering-reset-button').fireEvent('check-default');
				}
			}
		}
	}).inject(crossInspectDialog);
	new Element('span', { html: ' ' }).inject(crossInspectDialog);
	var challengeNumberingButton = new Element('img', {
		src: images.reload,
		id: 'UCPANG-cross-inspect-challenge-numbering-reset-button',
		styles: {
			cursor: 'pointer',
			height: '14px'
		},
		events: {
			click: function (evt) {
				GM_deleteValue('UCPA.CrossCheckChallengeNumberingRegExp.' + groupConfig.groupname());
				$('UCPANG-cross-inspect-challenge-numbering-regexp').set('value', groupConfig.challengeNumberingRegExp());
				this.fireEvent('check-default');
			},
			'check-default': function (evt) {
				var groupValue = groupConfig.options.challengeNumberingRegExp;
				if (!$chk(groupValue) || groupValue.length == 0) {
					this.setStyles({ opacity: 0.3 });
					this.set('title', 'default value not configured (yet) for this group');
				} else {
					var currentValue = $('UCPANG-cross-inspect-challenge-numbering-regexp').get('value');
					if (currentValue == groupValue) {
						this.setStyles({ opacity: 0.3, cursor: '' });
						this.set('title', 'using group\'s default value');
					} else {
						this.setStyles({ opacity: 1.0, cursor: 'pointer' });
						this.set('title', 'reset to group\'s default value');
					}
				}
			}
		}
	}).inject(crossInspectDialog);
	challengeNumberingButton.fireEvent('check-default');
	new Element('span', { html: ' ' }).inject(crossInspectDialog);
	new Element('img', {
		src: images.info,
		styles: {
			cursor: 'pointer',
			height: '14px'
		}
	}).inject(new Element('a', {
		href: 'http://www.flickr.com/groups/unified_checkplay/discuss/72157623759768824/#cross-inspect-challenge-numbering',
		target: '_blank',
		title: 'click to get more information',
		styles: {
			backgroundColor: 'transparent'
		}
	}).inject(crossInspectDialog));
	new Element('div', {
		id: 'UCPANG-cross-inspect-challenge-numbering-detail',
		styles: {
			visibility: 'hidden',
			display: 'none'
		}
	}).inject(crossInspectDialog);
	new Element('br').inject(crossInspectDialog);

	// similar themes
	if ($chk(groupConfig.similarThemesRegExp())) {
	    new Element('img', {
		src: images.down,
		id: 'UCPANG-cross-inspect-similar-themes',
		title: 'click to ' + checkLabels.similarThemes.label,
		alt: checkLabels.similarThemes.value,
		'class': 'UCPANG-collapsed',
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				var detail = $('UCPANG-cross-inspect-similar-themes-detail');
				if (this.hasClass('UCPANG-collapsed')) {
					this.set('class', 'UCPANG-expanded');
					this.set('src', images.up);
					var regExp = $('UCPANG-cross-inspect-similar-themes-regexp').value;
					detail.setStyles( { visibility: 'visible', display: 'block' });
					if (!$chk(regExp)) {
						new Element('label', {
							html: 'can not check without a regular expression',
							styles: {
								color: 'red'
							}
						}).inject(detail);
						return;
					}
					var similarThemesRegExp = new RegExp(groupConfig.similarThemesRegExp());
					//similarThemesRegExp.compile(groupConfig.similarThemesRegExp());
					//GM_log("DEBUG: compiled='" + similarThemesRegExp + "'");
					if (!$chk(similarThemesRegExp)) {
						new Element('label', {
							html: 'invalid regular expression',
							styles: {
								color: 'red'
							}
						}).inject(detail);
						return;
					}
					var similarThreads = [];
					var skippedThreads = [];
					var nonChallengeThreads = [];
					var nonMatchingThreads = [];
					ucpThreads.filter(function (ucpThread) { // only check threads that are OPEN or VOTE
						if (!(ucpThread instanceof UCPChallengeThread)) {
							ucpThread.resetStatus();
							nonChallengeThreads.push(ucpThread);
							return false;
						}
						ucpThread.checkStatus();
						switch(ucpThread.challengeStatus()) {
							case "Open":
							case "Filled":
							case "--VOTE--":
								if (ucpThread.challengeName().test(similarThemesRegExp)) {
									return true;
								} else {
									nonMatchingThreads.push(ucpThread);
									return false;
								}
							default:
								skippedThreads.push(ucpThread);
								return false; // ignore [Finished] ones too
						}
					}).each(function (ucpThread) {
						var foundMatch = false;
						var ucpTheme = normalizeTheme(ucpThread.challengeName().match(similarThemesRegExp)[1]);
						similarThreads.each(function (similarCollection) {
							if (foundMatch) return;
							similarCollection.each(function (scannedThread) {
								if (foundMatch) return;
								var scannedTheme = normalizeTheme(scannedThread.challengeName().match(similarThemesRegExp)[1]);
								if (scannedTheme == ucpTheme) {
									foundMatch = true;
									return;
								}
								if (scannedTheme.length > 6 && ucpTheme.length > 6 &&
								    levenshtein(scannedTheme, ucpTheme) < 5) {
									foundMatch = true;
									return;
								}
								if (scannedTheme.length > 0 && ucpTheme > 0) { // empty string is part of any other string
									if (scannedTheme.indexOf(ucpTheme) != -1) { // part of
										foundMatch = true;
									}
									if (ucpTheme.indexOf(scannedTheme) != -1) { // part of
										foundMatch = true;
									}
								}
							});
							if (foundMatch) similarCollection.push(ucpThread);
						});
						if (!foundMatch) { // no similar one found
							similarThreads.push([ ucpThread ]); // store it in its own slot
						}
					});
					var similarityList = new Element('ul').inject(detail);
					var uniqueThemeThreads = [];
					if (similarThreads.length > 0 && 
						similarThreads.length + nonChallengeThreads.length + skippedThreads.length + nonMatchingThreads.length != ucpThreads.length) { // => at least one entry in similarThreads contains 2 or more threads
						similarThreads.each(function (similarCollection) {
							if (similarCollection.length > 1) {
								var item = new Element('li', { html: 'similar to <i>' + similarCollection[0].challengeName().match(similarThemesRegExp)[1] + '</i>:' }).inject(similarityList);
								var itemList = new Element('ul').inject(item);
								similarCollection.sort(function (thread1, thread2) {
									var name1 = thread1.challengeName();
									var name2 = thread2.challengeName();
									return name1 < name2 ? -1 : (name2 < name1 ? 1 : 0);
								}).each(function(similarThread) {
									new Element('a', {
										html: similarThread.challengeName(),
										href: '_blank'
									}).inject(new Element('li').inject(itemList));
								});
							} else {
								uniqueThemeThreads.push(similarCollection[0]);
							}
						});
					}
					if (!$chk(similarityList.getElement('li'))) {
						new Element('li', { html: 'no similar themes found' }).inject(similarityList);
						uniqueThemeThreads = similarThreads.map(function (similarCollection) {
							return similarCollection[0];
						});
					}

					[{ threads: nonChallengeThreads, label: 'non-challenge threads (chat, showroom, ..):', color: '' },
					 { threads: skippedThreads,      label: 'irrelevant threads (closed, voided, ..):', color: '' },
					 { threads: uniqueThemeThreads,  label: 'unique theme threads:', color: '' },
					 { threads: nonMatchingThreads,  label: 'non-matching threads:', color: 'brown' }].each(function (th) {
						if (th.threads.length > 0) {
							var item = new Element('li', { 
								html: '<font color=' + th.color + '>found ' + th.threads.length + ' ' + th.label + '</font>',
								styles: {
									cursor: 'pointer'
								},
								events: {
									click: function (evt) {
										if ($chk(this.getElement('ul'))) {
											this.getElement('ul').destroy();
											return;
										}
										var itemList = new Element('ul').inject(this);
										th.threads.sort(function (a,b) {
											var nameA = a.challengeName().toLowerCase( );
											var nameB = b.challengeName().toLowerCase( );
											if (nameA < nameB) {return -1}
											if (nameA > nameB) {return 1}
											return 0;
										}).each(function (ucpThread) {
											var threadItem = new Element('li').inject(itemList);
											new Element('a', {
												html: ucpThread.challengeName(),
												href: '_blank'
											}).inject(threadItem);
											if ($chk(ucpThread.challengeStatus)) { // function check
												new Element('span', {
													html: ' == ' + ucpThread.challengeStatus()
												}).inject(threadItem);
											}
										});	
									}
								}
							}).inject(similarityList);
						}
					});
				} else {
					this.set('class', 'UCPANG-collapsed');
					this.set('src', images.down);
					detail.empty();
					detail.setStyles({ visibility: 'hidden', display: 'none' });
				}
			}
		}
	    }).inject(crossInspectDialog);
	    new Element('label', {
		id: 'UCPANG-cross-inspect-similar-themes-label',
		html: ' ' + checkLabels.similarThemes.label + ' ',
		title: 'click to ' + checkLabels.similarThemes.label,
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				$('UCPANG-cross-inspect-similar-themes').fireEvent('click');
			}
		}
	    }).inject(crossInspectDialog);
	    new Element('input', {
		id: 'UCPANG-cross-inspect-similar-themes-regexp',
		value: groupConfig.similarThemesRegExp(),
		'class': 'UCPANG-disabled',
		disabled: true
	    }).inject(crossInspectDialog);
	    new Element('span', { html: ' ' }).inject(crossInspectDialog);
	    new Element('img', {
		src: images.edit,
		title: 'click to edit regular expression',
		styles: {
			cursor: 'pointer',
			height: '14px'
		},
		events: {
			click: function (evt) {
				var input = $('UCPANG-cross-inspect-similar-themes-regexp');
				var toEditmode = input.hasClass('UCPANG-disabled');
				input.set('class', toEditmode ? 'UCPANG-enabled' : 'UCPANG-disabled');
				input.set('disabled', !toEditmode);
				this.set('src', toEditmode ? images.save : images.edit);
				this.set('title', 'click ' + ( toEditmode ? 'to save' : 'to edit' ) + ' regular expression');
				// save?
				if (!toEditmode) { // clicked save
					GM_setValue('UCPA.CrossCheckSimilarThemesRegExp.' + groupConfig.groupname(), input.value);
					if (input.value == groupConfig.options.similarThemesRegExp || input.value.length == 0) {
						GM_deleteValue('UCPA.CrossCheckSimilarThemesRegExp');
					}
					$('UCPANG-cross-inspect-similar-themes-reset-button').fireEvent('check-default');
				}
			}
		}
	    }).inject(crossInspectDialog);
	    new Element('span', { html: ' ' }).inject(crossInspectDialog);
	    var similarThemesResetButton = new Element('img', {
		src: images.reload,
		id: 'UCPANG-cross-inspect-similar-themes-reset-button',
		styles: {
			cursor: 'pointer',
			height: '14px'
		},
		events: {
			click: function (evt) {
				GM_deleteValue('UCPA.CrossCheckSimilarThemesRegExp.' + groupConfig.groupname());
				$('UCPANG-cross-inspect-similar-themes-regexp').set('value', groupConfig.similarThemesRegExp());
				this.fireEvent('check-default');
			},
			'check-default': function (evt) {
				var groupValue = groupConfig.options.similarThemesRegExp;
				if (!$chk(groupValue) || groupValue.length == 0) {
					this.setStyles({ opacity: 0.3 });
					this.set('title', 'default value not configured (yet) for this group');
				} else {
					var currentValue = $('UCPANG-cross-inspect-similar-themes-regexp').get('value');
					if (currentValue == groupValue) {
						this.setStyles({ opacity: 0.3, cursor: '' });
						this.set('title', 'using group\'s default value');
					} else {
						this.setStyles({ opacity: 1.0, cursor: 'pointer' });
						this.set('title', 'reset to group\'s default value');
					}
				}
			}
		}
	    }).inject(crossInspectDialog);
	    similarThemesResetButton.fireEvent('check-default');
	    new Element('span', { html: ' ' }).inject(crossInspectDialog);
	    new Element('img', {
		src: images.info,
		styles: {
			cursor: 'pointer',
			height: '14px'
		}
	    }).inject(new Element('a', {
		href: 'http://www.flickr.com/groups/unified_checkplay/discuss/72157623759768824/#cross-inspect-similar-themes',
		target: '_blank',
		title: 'click to get more information',
		styles: {
			backgroundColor: 'transparent'
		}
	    }).inject(crossInspectDialog));
	    new Element('span', {
	    	html: ' (themes are considered \'similar\' when their spelling is similar, not their meaning)',
		styles: {
			fontStyle: 'italic'
		}
	    }).inject(crossInspectDialog);
	    new Element('div', {
		id: 'UCPANG-cross-inspect-similar-themes-detail',
		styles: {
			visibility: 'hidden',
			display: 'none'
		}
	    }).inject(crossInspectDialog);
	    new Element('br').inject(crossInspectDialog);
	} // if similarThemesExpression

function setLength(set) {
	var length = 0;
	for (var t in set) {
		if (set.hasOwnProperty(t)) {
			++length;
		}
	}
	return length;
}

	// count player entries
	new Element('img', { 
		src: images.down, 
		id: 'UCPANG-cross-inspect-playerentry-count',
		title: 'click to ' + checkLabels.playerEntries.label,
		alt: checkLabels.playerEntries.value,
		'class': 'UCPANG-collapsed',
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				var detail = $('UCPANG-cross-inspect-player-entry-detail');
				if (this.hasClass('UCPANG-collapsed')) {
					this.set('class', 'UCPANG-expanded');
					this.set('src', images.up);
					detail.setStyles({ visibility: 'visible', display: 'block' });
					var playerEntryCount = new Hash();
					ucpThreads.filter(function (ucpThread) {
						if (groupConfig.skipChallenge(ucpThread.challengeName())) {
							return false;
						}
						if (ucpThread instanceof UCPNonChallengeThread) {
							return false;
						}
						ucpThread.checkStatus();
						switch (ucpThread.challengeStatus()) {
							case "Closed":
							case "Voided":
							case "Expired":
								return false;
							default:
								return true;
						}
					}).each( function (ucpThread) {
						ucpThread.photos().each( function (competitor) {
							var playerentries = playerEntryCount.get(competitor.poster().userid);
							if (!$chk(playerentries)) {
								playerentries = { username: competitor.poster().username, challenges: new Hash() };
								playerEntryCount.set(competitor.poster().userid, playerentries);
							}
							if (!playerentries.challenges.has(ucpThread.topic())) {
								playerentries.challenges.set(ucpThread.topic(), ucpThread);
							}
						});
					});
					var threshold = 0;
					if ($('UCPANG-cross-inspect-player-entry-threshold-check').checked) {
						threshold = $('UCPANG-cross-inspect-player-entry-threshold').value;
						if (!$chk(threshold) || isNaN(threshold)) {
							threshold = 0;
						}
					}
					var expandAll = new Element('div', {
						html: '&emsp;&emsp;<u>click to expand all</u> ',
						'class': 'UCPANG-collapsed',
						styles: {
							cursor: 'pointer',
							fontStyle: 'italic',
							visibility: 'hidden',
							display: 'none'
						},
						events: {
							click: function(evt) {
								this.getParent().getElement('ul').getElements('li img.UCPANG-collapsed').each( function (img) {
									img.fireEvent('click');
								});
							}
						}
					}).inject(detail);
					var playerList = new Element('ul').inject(detail);
					playerEntryCount.getValues().sort(function (value1, value2) {
						return value2.challenges.getLength() - value1.challenges.getLength();
					}).each(function (value) {
						if (value.challenges.getLength() > threshold) {
							expandAll.setStyles({ visibility: 'visible', display: 'block' });
							var info = new Element('li').inject(playerList);
							new Element('label', {
								html: '<b>' + value.username + '</b> has entries in <b>' + value.challenges.getLength() + '</b> of the selected challenges ',
								'class': 'UCPANG-collapsed',
								styles: {
									cursor: 'pointer'
								},
								events: {
									click: function (evt) {
										if (this.hasClass('UCPANG-collapsed')) {
											this.set('class', 'UCPANG-expanded');
											info.getElement('img').set('src', images.up);
											info.getElement('img').set('class', 'UCPANG-expanded');
											var list = new Element('ul', {
											}).inject(info);
											value.challenges.getValues().sort(function (thread1, thread2) {
												var name1 = thread1.challengeName();
												var name2 = thread2.challengeName();
												return (name1 < name2 ? -1 : (name2 < name1 ? 1 : 0));
											}).each(function (challenge) {
												new Element('a', {
													href: challenge.url(),
													html: challenge.challengeName(),
													target: '_blank'
												}).inject(new Element('li').inject(list));
											});
										} else {
											this.set('class', 'UCPANG-collapsed');
											this.getParent('li').getElement('img').set('src', images.down);
											this.getParent('li').getElement('img').set('class', 'UCPANG-collapsed');
											this.getParent('li').getElement('ul').destroy();
										}
									}
								}
							}).inject(info);
							new Element('img', {
								src: images.down,
								'class': 'UCPANG-collapsed',
								styles: {
									cursor: 'pointer'
								},
								events: {
									click: function (evt) {
										this.getParent('li').getElement('label').fireEvent('click');
									}
								}
							}).inject(info);
						}
					});
					if (playerList.getChildren().length == 0) {
						playerList.destroy();
						new Element('label', { html: '--none found--' }).inject(detail);
					}
				} else {
					this.set('class', 'UCPANG-collapsed');
					this.set('src', images.down);
					detail.empty();
					detail.setStyles({ visibility: 'hidden', display: 'none' });
				}
			}
		} 
	}).inject(crossInspectDialog);
	new Element('label', {
		id: 'UCPANG-cross-inspect-playerentry-label',
		html: ' ' + checkLabels.playerEntries.label + ' ',
		title: 'click to ' + checkLabels.playerEntries.label,
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				$('UCPANG-cross-inspect-playerentry-count').fireEvent('click');
			}
		}
	}).inject(crossInspectDialog);
	// threshold
	new Element('label', { html: ' (' }).inject(crossInspectDialog);
	var usedThresholdLastInvocation = GM_getValue('UCPA.CrossCheckUsedPlayerEntryThreshold.' + groupConfig.groupname());
	usedThresholdLastInvocation = usedThresholdLastInvocation === true || usedThresholdLastInvocation === 'true';
	new Element('input', {
		type: 'checkbox',
		id: 'UCPANG-cross-inspect-player-entry-threshold-check',
		checked: usedThresholdLastInvocation,
		events: {
			change: function (evt) {
				GM_setValue('UCPA.CrossCheckUsedPlayerEntryThreshold.' + groupConfig.groupname(), this.checked);
			}
		}
	}).inject(crossInspectDialog);
	new Element('label', {
		'for': 'UCPANG-cross-inspect-player-entry-threshold-check',
		html: 'only show players with more than '
	}).inject(crossInspectDialog);
	new Element('input', {
		type: 'number',
		min: 1,
		id: 'UCPANG-cross-inspect-player-entry-threshold',
		value: GM_getValue('UCPA.CrossCheckPlayerEntryThreshold.' + groupConfig.groupname()),
		styles: {
			width: '35px'//,
			// height: 15px,
			//font-size: 9px;
		},
		events: {
			change: function (evt) {
				GM_setValue('UCPA.CrossCheckPlayerEntryThreshold.' + groupConfig.groupname(), this.value);
			}
		}
	}).inject(crossInspectDialog);
	new Element('label', {
		html: ' entries)'
	}).inject(crossInspectDialog);
	if ($chk(groupConfig.groupLimit()) && groupConfig.groupLimit() > 0) {
		new Element('span', { 
			html: ' -- (this check does not (yet) warn for group limit violations)',
			styles: {
				fontStyle: 'italic'
			}
		}).inject(crossInspectDialog);
	}
	new Element('span', { html: ' ' }).inject(crossInspectDialog);
	new Element('img', {
		src: images.info,
		styles: {
			cursor: 'pointer',
			height: '14px'
		}
	}).inject(new Element('a', {
		href: 'http://www.flickr.com/groups/unified_checkplay/discuss/72157623759768824/#cross-inspect-count-player-entries',
		target: '_blank',
		title: 'click to get more information',
		styles: {
			backgroundColor: 'transparent'
		}
	}).inject(crossInspectDialog));
	new Element('div', {
		id: 'UCPANG-cross-inspect-player-entry-detail',
		styles: {
			visibility: 'hidden',
			display: 'none'
		}
	}).inject(crossInspectDialog);
	new Element('br').inject(crossInspectDialog);
	// double entries
	new Element('img', {
                src: images.down,
                id: 'UCPANG-cross-inspect-photo-count',
                title: 'click to ' + checkLabels.multiChallenge.label,
		alt: checkLabels.multiChallenge.value,
                'class': 'UCPANG-collapsed',
		styles: {
			cursor: 'pointer'
		},
                events: {
			click: function (evt) {
				var detail = $('UCPANG-cross-inspect-photo-count-detail');
				if (this.hasClass('UCPANG-collapsed')) {
					this.set('class', 'UCPANG-expanded');
					this.set('src', images.up);
					detail.setStyles({ visibility: 'visible', display: 'block' });
					var photoEntryCount = new Hash();
					ucpThreads.filter(function (ucpThread) {
						if (groupConfig.skipChallenge(ucpThread.challengeName())) {
							return false;
						}
						if (ucpThread instanceof UCPNonChallengeThread) {
							return false;
						}
						ucpThread.checkStatus();
						switch (ucpThread.challengeStatus()) {
							case "Closed":
							case "Voided":
							case "Expired":
								return false;
							default:
								return true;
						}
					}).each( function (ucpThread) {
						ucpThread.photos().each( function (competitor) {
							var photoentries = photoEntryCount.get(competitor.photoId());
							if (!$chk(photoentries)) {
								photoentries = { username: competitor.poster().username, photoId: competitor.photoId(), title: competitor.photoTitle(), challenges: [] };
								photoEntryCount.set(competitor.photoId(), photoentries);
							}
							if (!photoentries.challenges.some(function (challenge) {
									return challenge.topic() == ucpThread.topic();
								})) {
								photoentries.challenges.push(ucpThread);
							}
						});
					});
					var threshold = 2;
					var multiPhotos = photoEntryCount.getValues().sort(function (value1, value2) {
						return value2.challenges.length - value1.challenges.length;
					}).filter(function (photoEntry) {
						return photoEntry.challenges.length > 1;
					});
					if (multiPhotos.length > 0) {
						var list = new Element('ul').inject(detail);
						multiPhotos.each(function (value) {
							var info = new Element('li').inject(list);
							new Element('label', {
								html: 'photo <b>' + value.title + '</b> (id: ' + value.photoId + ', poster: ' + value.username + ') has been entered in <b>' + value.challenges.length + '</b> of the selected challenges ',
								'class': 'UCPANG-collapsed',
								events: {
									click: function (evt) {
										if (this.hasClass('UCPANG-collapsed')) {
											this.set('class', 'UCPANG-expanded');
											this.getParent('li').getElement('img').set('src', images.up);
											var threadList = new Element('ul', {
											}).inject(info);
											value.challenges.each(function (challenge) {
												new Element('a', {
													href: challenge.url(),
													html: challenge.challengeName(),
													target: '_blank',
													events: {
														click: function (evt) {
															$(evt).stopPropagation();
														}
													}
												}).inject(new Element('li').inject(threadList));
											});
										} else {
											this.set('class', 'UCPANG-collapsed');
											this.getParent('li').getElement('img').set('src', images.down);
											this.getParent('li').getElement('ul').destroy();
										}
									}
								},
								styles: { 
									cursor: 'pointer'
								}
							}).inject(info);
							new Element('img', {
								src: images.down,
								events: {
									click: function (evt) {
										this.getParent().getElement('label').fireEvent('click');
									}
								},
								styles: { 
									cursor: 'pointer'
								}
							}).inject(info);
						});
					} else {
						new Element('span', { html: '--none found--' }).inject(detail);
					}
				} else {
					this.set('class', 'UCPANG-collapsed');
					this.set('src', images.down);
					detail.empty();
					detail.setStyles({ visibility: 'hidden', display: 'none' });
				}
			}
		}
	}).inject(crossInspectDialog);
	new Element('label', {
		html: ' ' + checkLabels.multiChallenge.label,
		title: 'click to ' + checkLabels.multiChallenge.label,
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				$('UCPANG-cross-inspect-photo-count').fireEvent('click');
			}
		}
	}).inject(crossInspectDialog);
	new Element('span', { html: ' ' }).inject(crossInspectDialog);
	new Element('img', {
		src: images.info,
		styles: {
			cursor: 'pointer',
			height: '14px'
		}
	}).inject(new Element('a', {
		href: 'http://www.flickr.com/groups/unified_checkplay/discuss/72157623759768824/#cross-inspect-duplicates',
		target: '_blank',
		title: 'click to get more information',
		styles: {
			backgroundColor: 'transparent'
		}
	}).inject(crossInspectDialog));
	new Element('div', {
		id: 'UCPANG-cross-inspect-photo-count-detail',
		styles: {
			visibility: 'hidden',
			display: 'none'
		}
	}).inject(crossInspectDialog);
	new Element('br').inject(crossInspectDialog);
	// check for non-voters
	new Element('img', {
                src: images.down,
                id: 'UCPANG-cross-inspect-non-voters',
                title: 'click to ' + checkLabels.nonVoters.label,
		alt: checkLabels.nonVoters.value,
                'class': 'UCPANG-collapsed',
		styles: {
			cursor: 'pointer'
		},
                events: {
			click: function (evt) {
				var detail = $('UCPANG-cross-inspect-non-voters-detail');
				if (this.hasClass('UCPANG-collapsed')) {
					this.set('class', 'UCPANG-expanded');
					this.set('src', images.up);
					detail.setStyles({ visibility: 'visible', display: 'block' });
					new Element('img', { src: updatingIcon }).inject(detail);
					new Element('span', { html: ' processing..' }).inject(detail);
					// we only need to check the OPEN challenges: VOTE challenges have been approved, and should have been checked
					// => not all admods use the script => also check VOTE challenges
					var voteThreads = [];
					var openThreads = ucpThreads.filter(function (ucpThread) {
						if (groupConfig.skipChallenge(ucpThread.challengeName())) {
							return false;
						}
						if (ucpThread instanceof UCPNonChallengeThread) {
							return false;
						}
						ucpThread.checkStatus();
						if (ucpThread.challengeStatus().test('--VOTE--')) {
							//if (!ucpThread.finished()) {
								voteThreads.push(ucpThread);
							//}
							return false;
						}
						return true;
					});
					// a player that plays in another challenge, or has voted in that other challenge, need not be checked
					var nonVoters = new Hash();
					var votedThreads = new Hash();
					var playerThreads = new Hash();
					[openThreads,voteThreads].each(function (challengeThreads) {
					    challengeThreads.each(function (challengeThread) {
						challengeThread.photos().filter(function (competitor) {
							var posterId = competitor.poster().userid;
							var nonVotedThreads = voteThreads.filter(function (voteThread) {
								var isCompetitorInThread = voteThread.photos().some(function (competitor) { return competitor.poster().userid == posterId; });
								var isVoterInThread = voteThread.votes().some(function (vote) { return vote.poster().userid == posterId; });
								if (isVoterInThread) {
									var votedTopics = votedThreads.get(posterId);
									if (!$chk(votedTopics)) {
										votedTopics = {}; // implement as a set
										votedThreads.set(posterId, votedTopics);
									}
									votedTopics[ voteThread.topic() ] = true;
								}
								if (isCompetitorInThread) {
									var competitorTopics = playerThreads.get(posterId);
									if (!$chk(competitorTopics)) {
										competitorTopics = {}; // implement as a set
										playerThreads.set(posterId, competitorTopics);
									}
									competitorTopics[ voteThread.topic() ] = true;
								}
								return !isCompetitorInThread && !isVoterInThread
							});
							if (nonVotedThreads.length > 0) {
								var entries = nonVoters.get(posterId);
								if (!$chk(entries)) {
									entries = [];
									nonVoters.set(posterId, entries);
								}
								entries.push({ photo: competitor, nonVotedThreads: nonVotedThreads });
							}
						});
					    });
					});
					detail.empty();
					if (nonVoters.getLength() == 0) {
						new Element('span', { html: '--none found--' }).inject(detail);
					} else {
						try {
							var threshold = parseInt($('UCPANG-cross-inspect-non-voters-threshold').value, 10);
						} catch (e) {
							threshold = 0;
						}
						
						if (threshold <= 0 || isNaN(threshold)) {
							new Element('div', {
								html: '--no check with empty or invalid threshold--',
								styles: {
									color: 'red'
								}
							}).inject(detail);
							return;
						}
						var beyondThreshold = new Hash();
						var withinThreshold = new Hash();
						var undecidedList = new Hash();
						nonVoters.getKeys().each(function (posterId) {
							nonVoters.get(posterId).each(function (nonVoter) {
								var postedTime = nonVoter.photo.node().getElement('small').get('text');
								postedTime = postedTime.replace(/\n/mg, '').replace(/^.*osted (\d+ \w+ ago).*$/, '$1');
								var beyondThresholdForPhoto = [];
								var withinThresholdForPhoto = [];
								var undecidedListForPhoto = [];
								nonVoter.nonVotedThreads.each( function (nonVotedThread) {
									var votingTime = nonVotedThread.challengeAnnouncementNode().getElement('small').get('text');
									votingTime = votingTime.replace(/\n/mg, '').replace(/^.*edited this topic (\d+ \w+ ago).*$/, '$1');
									try { // try to compare
										var postedTimeNumber = parseInt(postedTime, 10);
										var votingTimeNumber = parseInt(votingTime, 10);
										var timeConverter = [];
											timeConverter['second']  = 0;
											timeConverter['seconds'] = 0;
											timeConverter['minute']  = 1;
											timeConverter['minutes'] = 1;
											timeConverter['hour']    = 60;
											timeConverter['hours']   = 60;
											timeConverter['day']     = 60 * 24;
											timeConverter['days']    = 60 * 24;
											timeConverter['week']    = 60 * 24 * 7;
											timeConverter['weeks']   = 60 * 24 * 7;
											timeConverter['month']   = 60 * 24 * 7 * 30;
											timeConverter['months']  = 60 * 24 * 7 * 30;
										var postedTimeInMinutes = postedTimeNumber * timeConverter[postedTime.replace(/^.*\d+\s+(\w+)\s+.*$/,'$1')];
										var votedTimeInMinutes = votingTimeNumber * timeConverter[votingTime.replace(/^.*\d+\s+(\w+)\s+.*$/,'$1')];
										if (postedTimeInMinutes >= votedTimeInMinutes) {
											return;
										}
										if (postedTimeInMinutes <= threshold) {
											withinThresholdForPhoto.push({ nonVotedThread: nonVotedThread, votingTime: votingTime });
										} else {
											beyondThresholdForPhoto.push({ nonVotedThread: nonVotedThread, votingTime: votingTime });
										}
									} catch (e) {
										GM_log("DEBUG: error converting: " + e);
										undecidedListForPhoto.push({ nonVotedThread: nonVotedThread, postedTime: postedTime, votingTime: votingTime });
									}
								});
								[ { globalList: beyondThreshold, localList: beyondThresholdForPhoto },
								  { globalList: withinThreshold, localList: withinThresholdForPhoto },
								  { globalList: undecidedList, localList: undecidedListForPhoto }].each (function (data) {
									if (data.localList.length > 0) {
										var userEntry = data.globalList.get(nonVoter.photo.poster().userid);
										if (!$chk(userEntry)) {
											userEntry = { photos: [], nonVotedThreads: [] };
											data.globalList.set(nonVoter.photo.poster().userid, userEntry);
										}
										userEntry.photos.push({ photo: nonVoter.photo, postedTime: postedTime });
										userEntry.nonVotedThreads = data.localList; // they are the same for every photo
									}
								});
							});
						});
						[ { thresholdList: beyondThreshold, label: 'Passed the given threshold:', exact: true, severe: true  },
						  { thresholdList: withinThreshold, label: 'Still within the given threshold:', exact: true, severe: false },
						  { thresholdList: undecidedList, label: 'Failed calculating threshold:', exact: false, severe: false } ].each (function (th) {
							if (th.thresholdList.getLength() > 0) {
								new Element('span', {
									html: '&emsp;' + th.label,
									styles: {
										fontWeight: 'bold',
										color: th.severe ? 'red' : ''
									}
								}).inject(detail);
								var userList = new Element('ul').inject(detail);
								th.thresholdList.getKeys().each(function (posterId) { // 'sorted' by user
									var userEntry = th.thresholdList.get(posterId);
										var photo = userEntry.photo;
										var postedTime = userEntry.postedTime;
										var nonVotedThreads = userEntry.nonVotedThreads;
									
									var userItem = new Element('li', {
									}).inject(userList);
									new Element('label', {
										html: '<b>' + userEntry.photos[0].photo.poster().username + '</b> posted an entry '
									}).inject(userItem);
									var photoDetail = new Element('ul').inject(userItem);
									userEntry.photos.each(function (photo) {
										var postedTime = photo.postedTime;
										var competitor = photo.photo;
										var photoElement = new Element('li').inject(photoDetail);
										new Element('label', {
											html:  postedTime + ' in <i>'
										}).inject(photoElement);
										new Element('a', {
											href: competitor.ucpThread().url(),
											html: competitor.ucpThread().challengeName(),
											target: '_blank'
										}).inject(photoElement);
										new Element('label', {
											html: '</i>'
										}).inject(photoElement);
									});
									new Element('label', {
										html: 'but has ' + (th.exact ? '<u>' : 'probably ') + 'not' + (th.exact ? '</u>' : ' ') + ' (yet) voted in <i>',
										styles: {
											color: th.severe ? 'red' : ''
										}
									}).inject(userItem);

									var violation = new Element('ul').inject(userItem);
									nonVotedThreads.each(function (nvt) {
										var votingTime = nvt.votingTime;
										var nonVotedThread = nvt.nonVotedThread;
										var nvtElement = new Element('li').inject(violation);
										new Element('a', {
											href: nonVotedThread.url(),
											html: nonVotedThread.challengeName(),
											target: '_blank'
										}).inject(nvtElement);
										new Element('label', {
											html: '</i>'
										}).inject(nvtElement);
										new Element('label', {
											html: '<br/>which was presumably set to VOTE ' + votingTime
										}).inject(nvtElement);
									});
									var votedThreadsForPoster = votedThreads.get(posterId);
									if (!$chk(votedThreadsForPoster) || setLength(votedThreadsForPoster) <= 0) {
										new Element('div', {
											html: '=> has not voted in any other VOTE thread'
										}).inject(userItem);
									} else {
										new Element('div', {
											html: '=> but has voted in ' + setLength(votedThreadsForPoster) + ' other VOTE threads'
										}).inject(userItem);
									}
									var playerThreadsForPoster = playerThreads.get(posterId);
									if ($chk(playerThreadsForPoster) && setLength(playerThreadsForPoster) > 0) {
										new Element('div', {
											html: '=> plays in ' + setLength(playerThreadsForPoster) + ' other VOTE threads'
										}).inject(userItem);
									}
									new Element('p').inject(userItem);
								});
							}
						});
						if (beyondThreshold.getLength() == 0 && withinThreshold.getLength() == 0 && undecidedList.getLength() == 0) {
							new Element('span', {
								html: '--no violators found--'
							}).inject(detail);
						}
					}
				} else {
					this.set('class', 'UCPANG-collapsed');
					this.set('src', images.down);
					detail.empty();
					detail.setStyles({ visibility: 'hidden', display: 'none' });
				}
			}
		}
	}).inject(crossInspectDialog);
	new Element('label', {
		html: ' ' + checkLabels.nonVoters.label + ' (timeframe to vote in: ',
		title: 'click to ' + checkLabels.nonVoters.label,
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function (evt) {
				$('UCPANG-cross-inspect-non-voters').fireEvent('click');
			}
		}
	}).inject(crossInspectDialog);
	new Element('input', {
		type: 'number',
		min: 0,
		id: 'UCPANG-cross-inspect-non-voters-threshold',
		value: GM_getValue('UCPA.CrossCheckNonVotingThreshold.' + groupConfig.groupname()),
		styles: {
			width: '35px'
		},
		events: {
			change: function (evt) {
				if ($chk(this.value) && this.value.length > 0) {
					GM_setValue('UCPA.CrossCheckNonVotingThreshold.' + groupConfig.groupname(), this.value);
				} else {
					GM_deleteValue('UCPA.CrossCheckNonVotingThreshold.' + groupConfig.groupname());
				}
			}
		}
	}).inject(crossInspectDialog);
	new Element('label', {
		html: 'minutes)',
		events: {
			click: function (evt) {
				$('UCPANG-cross-inspect-non-voters').fireEvent('click');
			}
		}
	}).inject(crossInspectDialog);
	new Element('span', { html: ' ' }).inject(crossInspectDialog);
	new Element('img', {
		src: images.info,
		styles: {
			cursor: 'pointer',
			height: '14px'
		}
	}).inject(new Element('a', {
		href: 'http://www.flickr.com/groups/unified_checkplay/discuss/72157623759768824/#cross-inspect-non-voters',
		target: '_blank',
		title: 'click to get more information',
		styles: {
			backgroundColor: 'transparent'
		}
	}).inject(crossInspectDialog));
				
	new Element('div', {
		id: 'UCPANG-cross-inspect-non-voters-detail',
		styles: {
			visibility: 'hidden',
			display: 'none'
		}
	}).inject(crossInspectDialog);

	if (automode) { // click all buttons
		crossInspectDialog.getElements('img.UCPANG-collapsed').each(function (img) {
			if (img.get('alt').test('never')) return;
			img.fireEvent('click');
		});
	} else {
		crossInspectDialog.getElements('img.UCPANG-collapsed').each(function (img) {
			if (img.get('alt').test('always')) img.fireEvent('click');
		});
	}
    }

    function processTopicListingTable(topicListingTable) {
        var topicListingHeaderRow = topicListingTable.getElement('tr');
        var headerColumns = topicListingHeaderRow.getElements('th');
        var challengeColumnIdx = 0;
        var challengeColumnHeader = headerColumns[0];
        if (challengeColumnHeader.textContent.match('UCP')) {
            challengeColumnIdx = 1;
            challengeColumnHeader = headerColumns[1];
        }

	// allignment
	topicListingHeaderRow.setStyle('vertical-align', 'middle');
        var allCheckBoxes = [];
        var openCheckBoxes = [];
        var voteCheckBoxes = [];
        var nonClosedCheckBoxes = [];

        challengeColumnHeader.style.whiteSpace = 'nowrap';
        challengeColumnHeader.adopt(document.createTextNode(' | select '));
        var checkAllAnchor = challengeColumnHeader.adopt(new Element('a', {
                html: 'all',
                title: 'click to select all threads',
                styles: {
                    color: 'blue',
                    textDecoration: 'underline',
                    cursor: 'pointer'
                },
                events: {
                    'click': function () {
                        allCheckBoxes.each(function (checkbox) {
                            checkbox.checked = true;
                        })
                    }
                }
            }));

    if (challengeGroup) {
        challengeColumnHeader.adopt(document.createTextNode('  '));
        var checkOpenAnchor = challengeColumnHeader.adopt(new Element('a', {
                html: 'open',
                title: 'click to select threads in \'open\' status',
                styles: {
                    color: 'blue',
                    textDecoration: 'underline',
                    cursor: 'pointer'
                },
                events: {
                    'click': function () {
                        openCheckBoxes.each(function (checkbox) {
                            checkbox.checked = true;
                        })
                    }
                }
            }));

        challengeColumnHeader.adopt(document.createTextNode('  '));
        var checkVoteAnchor = challengeColumnHeader.adopt(new Element('a', {
                html: 'vote',
                title: 'click to select threads in \'vote\' status',
                styles: {
                    color: 'blue',
                    textDecoration: 'underline',
                    cursor: 'pointer'
                },
                events: {
                    'click': function () {
                        voteCheckBoxes.each(function (checkbox) {
                            checkbox.checked = true;
                        })
                    }
                }
            }));

        challengeColumnHeader.adopt(document.createTextNode('  '));
        var checkNonClosedAnchor = challengeColumnHeader.adopt(new Element('a', {
                html: 'non-closed',
                title: 'click to select threads that are not closed',
                styles: {
                    color: 'blue',
                    textDecoration: 'underline',
                    cursor: 'pointer'
                },
                events: {
                    'click': function () {
                        nonClosedCheckBoxes.each(function (checkbox) {
                            checkbox.checked = true;
                        })
                    }
                }
            }));

    }
        challengeColumnHeader.adopt(document.createTextNode(' | unselect '));
        var uncheckAllAnchor = challengeColumnHeader.adopt(new Element('a', {
                html: 'all',
                title: 'click to unselect all threads',
                styles: {
                    color: 'blue',
                    textDecoration: 'underline',
                    cursor: 'pointer'
                },
                events: {
                    'click': function () {
                        allCheckBoxes.each(function (checkbox) {
                            checkbox.checked = false;
                        })
                    }
                }
            }));

        challengeColumnHeader.adopt(document.createTextNode('  '));
        challengeColumnHeader.adopt(stickyButton = new Element('button', {
            'class': 'CancelButt',
            title: 'make selected threads sticky'
        }).adopt(
            new Element('span', { html: ' ' }),
            new Element('img', { src: images.sticky, height: 14 }),
            new Element('span', { html: ' ' })
        ));
        stickyButton.addEvent('click', function () {
            makeSticky(allCheckBoxes, stickyButton);
        });
        challengeColumnHeader.adopt(document.createTextNode('  '));
        challengeColumnHeader.adopt(bumpButton = new Element('button', {
            'class': 'CancelButt',
            title: 'bump selected threads',
            events: {
                'click': function () {
		    if (!challengeGroup) {
		    	var bumpMessage = $('UCPANG-bump-message').get('value');
			groupConfig.storeBumpMessage(bumpMessage);
		    }
                    var selectedCheckBoxes = [];
                    allCheckBoxes.each(function (checkBox) {
                        if (checkBox.checked) {
                            selectedCheckBoxes.push(checkBox);
                        }
                    });
                    if (selectedCheckBoxes.length > 0) {
                        bumpButton.getElement('img').set('src', updatingIcon);
			bumpButton.getElement('img').set('disabled', true);
                
                        startBumping(selectedCheckBoxes, bumpButton);
                    }
                }
            }
        })); // already needed here

        bumpButton.adopt(
            new Element('span', { html: ' ' }),
            new Element('img', { src: images.bump, height: 14 }),
            new Element('span', { html: ' ' })
        );

        challengeColumnHeader.adopt(new Element('span', { html: '&nbsp;' }));

    if (challengeGroup) {

        challengeColumnHeader.adopt(expandButton = new Element('button', {
            'class': 'CancelButt',
            title: 'expand/collapse selected threads'
        }).adopt(
            new Element('span', { html: ' ' }),
            new Element('img', { src: images.down, height: 14 }),
            new Element('img', { src: images.up, height: 14 }),
            new Element('span', { html: ' ' })
        ));
        expandButton.addEvent('click', function () {
            startExpanding(allCheckBoxes)
        });

        // cross inspect
        challengeColumnHeader.adopt(new Element('span', { html: '&nbsp;' }));

        challengeColumnHeader.adopt(crossInspectButton = new Element('button', {
            'class': 'CancelButt',
            title: 'cross-inspect selected threads'
        }).adopt(
            new Element('span', { html: ' ' }), // makes the button the right size
            new Element('img', { src: images.viewmag, height: 14 }),
            new Element('span', { html: ' ' }) // makes the button the right size
        ));
        crossInspectButton.addEvent('click', function () {
            startCrossInspect(allCheckBoxes)
        });

        var ucpDecoratorFactory = new UCPTopicListingThreadDecoratorFactory();
        var ucpReplyDecoratorFactory = new UCPTopicListingReplyDecoratorFactory();
    } else {
    	var bumpMessage = groupConfig.bumpMessage();
	new Element('label', {
	    html: ' bump message:'
	}).inject(challengeColumnHeader);
	var bumpMessageInput = new Element('input', {
	    type: 'text',
	    name: 'UCPANG-bump-message',
	    id: 'UCPANG-bump-message',
	    value: groupConfig.bumpMessage()
	}).inject(challengeColumnHeader);
    }

        var topicListingRows = topicListingTable.getElements('tr');
        var infoImg = new Element('input', {
            type: 'image',
            src: images.down,
            width: '12',
            title: 'show UCP Admin tool info',
            styles: {
                cursor: 'pointer'
            }
        });
        // let's loop the table and start processing
        $each(topicListingRows, function(topicRow, topicRowIdx) {
            if (topicRowIdx === 0) {// skip first: the header row
                return;
            }
            var columns = topicRow.getElements('td');
            var challengeColumn = columns[challengeColumnIdx]
            var topic = challengeColumn.getElement('a');
            var chlgname = topic.textContent;
            var challengeDefinition = groupConfig.extractChallengeDefinition(chlgname);
            var ucpThread = ucpCreateChallengeThread({
                groupConfig: groupConfig,
                url: topic.href,
                chlgname: chlgname,
                replyDecoratorFactory: ucpReplyDecoratorFactory,
                decoratorFactory: ucpDecoratorFactory
            });
            // TODO: check if we need to reread
            // ucpThread.retrieve();
	    if (challengeGroup) {
                var threadInfoImg = infoImg.clone();
                new Element('span', { html: ' ' }).inject(challengeColumn.getFirst(), 'before');
                threadInfoImg.inject(challengeColumn.getFirst(), 'before');
                // don't create a row below the challenge to show the data: should be moved with UCPstyle
                var infoCell = new Element('div', {
                    'class': 'UCPAInfo.basic',
                    styles: {
                        display: 'none',
                        visibility: 'hidden'
                    },
                    id: 'UCPANG.info.' + ucpThread.topic()
                }).inject(challengeColumn);
                // TODO: refresh button
                ucpThread.setFeedbackElement(infoCell);

                threadInfoImg.set('id', 'UCPANG.arrow.' + ucpThread.url());
                threadInfoImg.addEvent('click', function () {
                    var visible = infoCell.get('style').match(/block/);
                    if ($chk(visible)) {
                        threadInfoImg.src = images.down;
                        threadInfoImg.title = 'show UCP Admin tool info';
                        infoCell.set('style', 'display: none; visibility: hidden;');
                    } else {
                        threadInfoImg.src = images.up;
                        threadInfoImg.title = 'hide UCP Admin tool info';
                        infoCell.set('style', 'display: block; visibility: visible;' + ucpDialogStyleString);
                        if (infoCell.get('class').match('UCPAInfo.basic')) {
                            infoCell.set('class', 'UCPAInfo.full');
                            // create spinner in infoCell
                            var spinner = new Element('img', {
                                src: updatingIcon
                            }).inject(infoCell);
                            // replace down arrow with spinner
                            threadInfoImg.set('src', updatingIcon);
                            ucpThread.loadthread(groupPreferences, function(result) {
				if (!result.success) {
					spinner.set('src', errorIcon);
					threadInfoImg.set('src', images.up);
					new Element('span', { html: ' ' + result.msg, styles: { fontColor: 'red', fontWeight: 'bold' } }).inject(infoCell);
				} else {
					processDiscussionTopicInList(result.discussionTopic, result.ucpThread);
				}
			    });
                        }
                    }
                }, false);
	    }
            var checkbox = new Element('input', {
                type: 'checkbox',
                name: topic.href,
            });
            allCheckBoxes.push(checkbox);
	    if (challengeGroup) {
                if (!challengeDefinition.nonChallengeType() && 
                    (chlgname.match(groupConfig.states().open) || chlgname.match(groupConfig.states().waitingForEntries))) {
                    openCheckBoxes.push(checkbox);
                }
                if (!challengeDefinition.nonChallengeType() &&
                    (chlgname.match(groupConfig.states().vote))) {
                    voteCheckBoxes.push(checkbox);
                }
                if (challengeDefinition.nonChallengeType() ||
                    chlgname.match(groupConfig.states().open) ||
                    chlgname.match(groupConfig.states().waitingForEntries) ||
                    chlgname.match(groupConfig.states().vote)) {
                
                    nonClosedCheckBoxes.push(checkbox);
                }
	    }
            checkbox.inject(challengeColumn.getFirst(), 'before');
        });
    }    

    function addCheckButton(cells, labels, apiMethod, apiArguments, callbacks) {
        var waitingImg = new Element('img', { src: updatingIcon });
        var retval = [];
        for (var cellIdx = 0, cellLen = cells.length; cellIdx < cellLen; ++cellIdx) {
            var cell = cells[cellIdx];
            var label = labels[cellIdx];
            cell.adopt(newButton = new Element('button', {
                'class': 'CancelButt',
                html: label,
                events: {
                    'click': function () {
                        var lostButtons = [];
                        for (var c = 0; c < cellLen; ++c) {
                            lostButtons[c] = cells[c].getChildren('button').dispose();
                            cells[c].adopt(waitingImg.clone());
                        }
                        var apiData = {
                            api_key: GM_getMagisterLudi(),
                            auth_hash: GM_getAuthHash(),
                            auth_token: GM_getAuthToken(),
                            format: 'json',
                            method: apiMethod,
                            nojsoncallback: 1
                        };
                        for (var apiArgument in apiArguments) {
                            if (apiArguments.hasOwnProperty(apiArgument)) {
                                apiData[apiArgument] = apiArguments[apiArgument];
                            }
                        }
                        //var apiCallback = {};
                        //apiCallback[apiMethod.replace(/\./g, '_') + '_onLoad'] = 
                        new Request({
                            url: "http://www.flickr.com/",
                            onSuccess: function (responseText, responseXML) {
                                for (c = 0; c < cells.length; ++c) {
                                    cells[c].removeChild(cells[c].firstChild);
                                }
                                var result;
                                try {
                                    result = JSON.parse(responseText);
                                } catch (e) {
                                    result = eval('(' + responseText + ')');
                                }
                                //GM_log("result: " + responseText);
                                if (result.stat === 'fail') {
                                    cell.adopt(document.createTextNode("ERROR: " + result.code + " - " + result.message));
                                    return;
                                }
                                var data;
                                for (c = 0; c < cells.length; ++c) {
                                    data = result;
                                    var callback = callbacks[c];
                                    if (callback !== undefined) {
                                        callback(cells[c], data, lostButtons[c]);
                                    }
                                }
                            }
                        }).get("/services/rest/", apiData); // funtion, apiCallback
                    }
                }
            }));
            retval.push(newButton);
        } // for cells
        return retval;
    } // function

    function addAwardsCombo(cell, label, resultCell, defaultAwardsGroup, apiMethod, apiArguments, callback) {
        var waitingImg = new Element('img', {
            src: updatingIcon
        });
        
        var photoId = apiArguments.photo_id;
        var awardsCheckTable = new Element('table', {
            border: 0
        });
        cell.adopt(awardsCheckTable);
        awardsCheckTable.adopt(
            new Element('tr').adopt(
                new Element('td', { colspan: 2 }).adopt(
                    newButton = new Element('button', {
                        'class': 'CancelButt',
                        html: label
                    }),
                    document.createTextNode(' '),
                    (newCombo = new Element('select', { id: photoId+".select" })).adopt(
                        new Element('option', {
                            html: '-- Any --',
                            value: "Any"
                        })
                    )
                )
            )
        );
        $each(ucpGroupConfigReader.groupList(), function (group) {
            var showGroupInList = true;
            // don't show groups that should not be shown
            if ($chk(group.hideFromAwardsList)) {
                // TODO: allow if group == thisGroup
                showGroupInList = false;
            }
            // unless we're in (the pending items of) an award group, and the group happens to be it's primary group
            // (allows cascading)
            if ($chk(groupConfig.awardWinnersGroup()) && groupConfig.primaryGroup() === group.groupname) {
                showGroupInList = true;
            }
            if (showGroupInList) {
                newCombo.adopt(new Element('option', {
                    html: group.name,
                    value: group.groupname,
                    selected: (group.groupname === defaultAwardsGroup ? "selected" : "")
                }));
            }
        });
        awardsCheckTable.adopt(
            new Element('tr').adopt(
                new Element('td').adopt( // left column: hide & stop
                    new Element('input', {
                        type: 'checkbox',
                        checked: groupPreferences.hideNonWonMedals(),
                        id: "UCPANG:hidemedals_" + apiArguments.photo_id
                    }),
                    new Element('label', {
                        'for': "UCPANG:hidemedals_" + apiArguments.photo_id,
                        html: 'only show won medals'
                    }),
                    new Element('br'),
                    new Element('input', {
                        type: 'checkbox',
                        checked: groupPreferences.stopAtFirstFoundMedal(),
                        id: "UCPANG:stopatfirstmedal_" + apiArguments.photo_id
                    }),
                    new Element('label', {
                        'for': "UCPANG:stopatfirstmedal_" + apiArguments.photo_id,
                        html: 'stop at first found medal'
                    })
                ),
                new Element('td', { valign: 'top' } ).adopt( // right column: types of medals
                    new Element('span', {
                        html: 'only show:',
                        styles: {
                            display: 'block'
                        }
                    }),
                    new Element('input', {
                        type: 'checkbox',
                        id: 'UCPANG:sweepmedal_' + apiArguments.photo_id
                    }),
                    new Element('label', {
                        'for': "UCPANG:sweepmedal_" + apiArguments.photo_id,
                        html: 'sweep / unanimous medals'
                    }),
                    new Element('br'),
                    new Element('input', {
                        type: 'checkbox',
                        id: 'UCPANG:iconmedal_' + apiArguments.photo_id
                    }),
                    new Element('label', {
                        'for': "UCPANG:iconmedal_" + apiArguments.photo_id,
                        html: 'icon challenge medals'
                    })
                )
            )
        );
        newButton.addEvent('click', function (event) {
                event.preventDefault(); // on the 'pending items' page, the button click would trigger the form 'submit'
                resultCell.getChildren().dispose();
                var waitingClone = waitingImg.clone();
                resultCell.adopt(waitingClone);
                var selectedGroup = $(photoId+".select").value;
                var apiData = {
                    api_key: GM_getMagisterLudi(),
                    auth_hash: GM_getAuthHash(),
                    auth_token: GM_getAuthToken(),
                    format: 'json',
                    nojsoncallback: 1,
                    method: apiMethod
                };
                for (var apiArgument in apiArguments) {
                    if (apiArguments.hasOwnProperty(apiArgument)) {
                        apiData[apiArgument] = apiArguments[apiArgument];
                    }
                }
                //var apiCallback = {};
                //apiCallback[apiMethod.replace(/\./g, '_') + '_onLoad'] = 
                //    function (success, responseXML, responseText, params) {
                new Request({
                    url: "http://www.flickr.com/",
                    onSuccess: function (responseText, responseXML) {
                        var result;
                        try {
                            result = JSON.parse(responseText);
                        } catch (e) {
                            result = eval('(' + responseText + ')');
                        }
                        if (result.stat === 'fail') {
                            resultCell.adopt(document.createTextNode("ERROR: " + result.code + " - " + result.message));
                            return;
                        }
                        if (callback !== undefined) {
                            callback(resultCell, result, responseText, {
                                    selectedGroup: selectedGroup,
                                    hideNonWon: $("UCPANG:hidemedals_" + apiArguments.photo_id).checked, 
                                    stopAtFirstFound: $('UCPANG:stopatfirstmedal_' + apiArguments.photo_id).checked,
                                    sweepMedals: $('UCPANG:sweepmedal_' + apiArguments.photo_id).checked,
                                    iconMedals: $('UCPANG:iconmedal_' + apiArguments.photo_id).checked,
				    photoId: photoId
                                });
                        }
                        waitingClone.dispose();
                    }
                }).get("/services/rest", apiData);
            }); // addEvent
        return newButton;
    } // function

// the medals used in the photo information box
var UCPMedal = new Class({
    Implements: [ Options ],
    options: {
        name: undefined,
	replaced_with: undefined,
        img: undefined,
        imgmatch: undefined,
        imgRegExp: undefined,
        text: undefined,
        textRegExp: undefined,
        sweep: false,
        icon: false
    },
    initialize: function (options) {
        this.setOptions(options);
    },
    name: function () {
        return this.options.name;
    },
    img: function () {
        return this.options.img;
    },
    hasTextMatch: function () {
        return $chk(this.options.text) || $chk(this.options.textRegExp);
    },
    containedIn: function (text) {
        if ($chk(this.options.imgRegExp)) {
            return text.match(new RegExp(this.options.imgRegExp.expression, this.options.imgRegExp.flags));
        }
        if ($chk(this.options.imgmatch)) {
            return text.match(this.options.imgmatch);
        }
        if ($chk(this.options.textRegExp)) {
            return text.match(new RegExp(this.options.textRegExp.expression, this.options.textRegExp.flags));
        }
        if ($chk(this.options.text)) {
            return text.match(this.options.text);
        }
        return text.match(this.options.img);
    }
});

    var allAwardsCallback = function (awardsCell, comments, commentsAsText, options) {
        var selectedGroup = options.selectedGroup;
        var hideNonWon = options.hideNonWon;
        var stopAtFirstFound = options.stopAtFirstFound;
        var onlySweepMedals = options.sweepMedals;
        var onlyIconMedals = options.iconMedals;
	var photoId = options.photoId;

        var commentList = comments.comments;
        var localGroupname = groupConfig.groupname();
        var groupDefinitions = ucpGroupConfigReader.groupList();
        if ($chk(selectedGroup) && selectedGroup !== "Any") {
            // only show for a particular group
            var onlyGroupDef = groupDefinitions[selectedGroup];
            groupDefinitions = {};
            groupDefinitions[selectedGroup] = onlyGroupDef;
        }
        var foundAMedal = false;
        new UCPMedalListReader().checkForUpdates();
        var medals = new UCPMedalListReader().createMedalsList();
        $each(groupDefinitions, function (groupDefinition, idx) {
            if (groupDefinition.hideFromAwardsList) {
                return;
            }
            if (foundAMedal && stopAtFirstFound) {
                return;
            }
            var groupMedals = medals[idx];
            if (!$chk(groupMedals)) {
                return;
            }
            $each(groupMedals, function (medal, medal_id) {
	    // important: medals that replace older medals should be defined before the old ones
                if (foundAMedal && stopAtFirstFound) {
                    return;
                }
                if (!$chk(medal.name)) { // comment in 'medals' block
                    return;
                }
                if (onlyIconMedals && !medal.icon) {
                    return;
                }
                if (onlySweepMedals && !medal.sweep) {
                    return;
                }
                var wonThisMedal = false;
                var permalink = null;
                var time = null;
                var aMedal = new UCPMedal(medal);
                try {
                    if ($chk(commentList.comment) && // no need to replace '/' for text (HOF winner works without)
                        aMedal.containedIn(commentsAsText.replace(/\\\//g, '\/')) ) {
                            // important: special characters should be replaced with .* !!
                            // but this would result in 'unresponsive script' if searching commentsAsText
                            // => never use .* in medal.imgmatch!!
                        // with $each, and a return on wonThisMedal, all comments are processed
                        // with a for loop, the loop stops if wonThisMedal = true
                        for (var cIdx = 0, cLen = commentList.comment.length; cIdx < cLen && !wonThisMedal; ++cIdx) {
                            var comment = commentList.comment[cIdx];
                            var commentText = comment._content;
                            if (aMedal.containedIn(commentText)) {
                                wonThisMedal = true;
                                foundAMedal = true;
                                time = comment.datecreate;
                                // the given permalink does not work over multiple pages
                                // removing the '#', does
                                permalink = comment.permalink.replace('#', '');
                            }
                        }
                    }
                } catch (e) {
                    GM_log("error: " + e);
                }
		var medalId = medal.replaced_with ? 
			'ucp_medal_' + groupDefinition.groupId + '_' + photoId + '_' + groupMedals[medal.replaced_with].medalId :
			'ucp_medal_' + groupDefinition.groupId + '_' + photoId + '_' + medal.medalId;
                if (wonThisMedal || !hideNonWon) {
                    var medalImg;
		    var anchor = $(medalId);
		    if (!$chk(anchor)) { // not there => create it
	                    awardsCell.adopt(anchor = new Element('a', {
				id: medalId,
        	                target: "_blank"
                	    }));
	                    if ($chk(medal.img)) {
				anchor.adopt(medalImg = new Element('img', {
				    src: medal.img,
				    styles: {
					'max-height': 32
				    }
				}));
	                    } else {
        	                anchor.adopt(new Element('span', {
                	            html: medal.name.replace(' ', '&nbsp;'),
                        	    styles: {
                                	'white-space': 'nowrap'
	                            }
        	                }));
                	    }
		    } else {
		    	medalImg = anchor.getElement('img');
		    }
		}
                if (wonThisMedal) {
		    var title = (groupDefinition.groupname !== localGroupname ? 
					groupDefinition.name + ": " : '') +
					medal.name.replace(/&#39;/g,"\'") + ": " + new Date(time * 1000);
		    if (!anchor.hasClass('medal_won')) {
		    // if already clear for a newer version of the medal, leave as is
	                anchor.href = permalink;
        	        anchor.title = title;
			anchor.set('class', 'medal_won');
		    }
                    awardsCell.adopt(wonImg = new Element('img', {
                        src: "http://l.yimg.com/g/images/icon_check_small.png",
			title: title
                    }));
                    if (medalImg) {
                        medalImg.set('style', 'border: 3px solid green; max-height: 32; opacity: 1.0;');
                        wonImg.set('style', 'position: relative; left: -9px');
                    } else {
                        anchor.set('style', 'border: 3px solid green; opacity: 1.0;');
                        wonImg.set('style', 'position: relative; left: -6px');
                    }
                } else if (!hideNonWon) {
		    title = (groupDefinition.groupname !== localGroupname ? 
                                                    groupDefinition.name + ": " : '') +
                                            medal.name.replace(/&#39;/g,"\'") + " (not found)";
		    if (!anchor.hasClass('medal_won') && !medal.replaced_with) { // leave the newer title
	                anchor.title = title;
		    }
                    awardsCell.adopt(nonImg = new Element('img', {
                        src: errorIcon,
			title: title,
                        height: "12"
                    }));
                    if (medalImg) {
		    	if (!anchor.hasClass('medal_won')) {
	                        medalImg.set('style', 'border: 1px solid red; max-height: 32; opacity: 0.3');
			}
                        nonImg.set('style', 'position: relative; left: -9px');
                    } else {
		    	if (!anchor.hasClass('medal_won')) {
	                        anchor.set('style', 'border: 1px solid red; opacity: 0.3');
			}
                        nonImg.set('style', 'position: relative; left: -6px');
                    }
                }
            });
        });
        if (awardsCell.getChildren().length <= 1) { // spinImage
            awardsCell.adopt(awardsCellImg = new Element('img', {
                src: errorIcon,
                height: "12"
            }));
            if (!groupPreferences.hideNonWonMedals() && !hideNonWon) {
                awardsCellImg.title = "no awards defined";
            } else {
                awardsCellImg.title = "no awards found or none defined";
            }
	}
    };

    function drawPhotoInformationBox(ucpCompetitor) {
        var photo = ucpCompetitor.photo();
        var photoRow = photo.getParent('tr');
        var photoParagraph = photoRow.getElement('td.Said');
        var photoId = ucpCompetitor.photoId();
        if ($chk($('UCPANG:' + photoId + '.table'))) { // it's already there
            return;
        }
        // TODO: ucpCompetitor should hold this info
        var playerBuddyIcon = photoRow.getElement('td.Who').getElement('img');
        if (!playerBuddyIcon || !ucpCheckBuddyicon(playerBuddyIcon)) {
            // if a user has no buddy icon, flickr provides a default one
            photoParagraph.adopt(new Element('small', {
                html: "no buddy icon found, aborting"
            }))
            return;
        }
        var playerId;
        try {
            playerId = playerBuddyIcon.get('src').match(/http:\/\/.*flickr.com\/\d+\/buddyicons\/(\d+@\w+).jpg/)[1];
        } catch (e) {
            try {
                // if the user has no buddy icon, flickr provides one: http://l.yimg.com/g/images/buddyicon.jpg#14164718@N03
                playerId = playerBuddyIcon.get('src').match(/http:\/\/.*\/buddyicon.(?:gif|jpg)#(\d+@\w+)/)[1];
            } catch (e) {
                photoParagraph.adopt(new Element('small', {
                    html: "invalid buddy icon found, aborting"
                }));
                return;
            }
        }
        // the 'Inline editor' from steev considers everything that comes before the first 'small' element as content
        //var firstSmall = photoParagraph.getElement('small');
        var outerDiv = new Element('div', {
            id: "UCPANG:" + photoId + ".table"
        }).inject(new Element('p', {
            border: '0',
            styles: ucpDialogStyle,
            'class': "UCPANG:" + photoId + ".p" // to be able to remove in approve function
        }).inject(new Element('td', { 
            colspan: 2 
        }).inject(new Element('tr', {
            id: 'UCPANG:' + photoId + '.row'
        }).inject(photoRow, 'after'))));
        // headers
        var basicButtons = {};
        outerDiv.adopt(new Element('tr').adopt(
                        new Element('th', { html: ' ' }),
                        new Element('th', { html: 'taken', title: 'date taken' }),
                        new Element('th', { html: 'public', title: 'photo is public?' }),
                        new Element('th', { html: 'edit', title: 'can comment, can add tags?' }),
                        new Element('th', { html: 'popularity', title: 'popularity: views, comments, favs', colspan: 3 }),
                        new Element('th', { html: 'pool', title: 'photo in pool?' }),
                        new Element('th', { html: 'awards', width: '100%', title: 'awards' }),
                        new Element('th', { align: 'right' }).adopt(new Element('a', {
                                target: '_blank',
                                html: 'help',
                                href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623759768824/#comment72157623782159508',
                                styles: {
                                    cursor: 'help'
                                }
                            }))
                    ),
                    new Element('tr', { vAlign: 'top' }).adopt(
                        new Element('td', { styles: { padding: 1 } }).adopt(
                            new Element('button', {
                                'class': 'CancelButt check-all-button',
                                html: 'check all',
                                events: {
                                    click: function () {
                                        this.dispose();
                                        basicButtons[0].click();
                                        favoritesButton[0].click();
                                        poolButton[0].click();
                                        allAwardsButton.click();
                                    }
                                }
                            })
                        ),
                        takenCell = new Element('td', { styles: { padding: 1 } }),
                        publicCell = new Element('td', { styles: { padding: 1 } }),
                        canCommentCell = new Element('td', { styles: { padding: 1 } }),
                        nViewsCell = new Element('td', { styles: { padding: 1 } }),
                        nCommentsCell = new Element('td', { styles: { padding: 1 } }),
                        favoritesCell = new Element('td', { styles: { padding: 1 } }),
                        poolCell = new Element('td', { styles: { padding: 1 } }),
                        awardsCell = new Element('td', { styles: { padding: 1 } }),
                        helpCell = new Element('td', { styles: { padding: 1 } })
                    ),
                    new Element('tr', {
		    		'valign': 'top'
		    	}).adopt(
	                        thumbnailCell = new Element('td'),
        	                allAwardsCell = new Element('td', { colSpan: 9 })
                    ),
                    new Element('tr').adopt(
                        exifCell = new Element('td')
                    )
                );
        var imgClone = photo.clone();
        imgClone.setAttribute("style", "max-width: 75; max-height: 75");
        imgClone.setAttribute("alt", "UCPthumnail");
	imgClone.removeAttribute("width");
	imgClone.removeAttribute("height");
        thumbnailCell.adopt(imgClone);

        var takenDateCallback = function (takenCell, data) {
            var time = data.photo.dates.taken;
            takenCell.empty().set('html', time);
        };
        var isPublicCallback = function (publicCell, data) {
            var ispublic = data.photo.visibility.ispublic;
            publicCell.adopt(new Element('img', {
                src:   (ispublic === 1 ? 
                            "http://l.yimg.com/g/images/icon_check_small.png" : 
                            errorIcon),
                title: (ispublic === 1 ? "photo is public" : "photo is NOT public"),
                height: "12"
            }));
        };
        var canCommentCallback = function (canCommentCell, data) {
            canCommentCell.valign = "top";
            var cancomment = data.photo.editability.cancomment;
            var commentImg = canCommentCell.adopt(new Element('img', {
                    src: cancomment === 1 ? "http://l.yimg.com/g/images/icon_check_small.png" : 
                                            errorIcon,
                    title: cancomment === 1 ? "can comment on photo" : "can NOT comment on photo",
                    height: "12"
                })
            );
            var cantag = data.photo.editability.canaddmeta;
            var tagImg = canCommentCell.adopt(new Element('img', {
                    src: cantag === 1 ? "http://l.yimg.com/g/images/icon_check_small.png" :
                                        errorIcon,
                    title: cantag === 1 ? "can add tags" : "can NOT add tags",
                    height: "12"
                })
            );
        };
        var nViewsCallback = function (numberOfViewsCell, data) {
            var views = data.photo.views;
            if ($chk(views)) {
                numberOfViewsCell.adopt(document.createTextNode(views + "v"));
            } else {
                numberOfViewsCell.adopt(document.createTextNode('0v'));
            }
            numberOfViewsCell.title = 'views';
        };
        var nCommentsCallback = function (numberOfCommentsCell, data) {
            var comments = data.photo.comments._content;
            if ($chk(comments)) {
                numberOfCommentsCell.adopt(document.createTextNode(comments + "c"));
            } else {
                numberOfCommentsCell.adopt(document.createTextNode('0c'));
            }
            numberOfCommentsCell.title = 'comments';
        };
        var exifCallback = function (exifCell, exifData, exifButton) {
            exifCell.adopt(exifButton);
            exifDataDiv = $('UCPANG:exifdata');
            if ($chk(exifDataDiv)) {
                exifDataDiv.empty();
            } else {
                exifDataDiv = new Element('div', {
                    id: 'UCPANG:exifdata',
                    styles: {
                        overflow: 'auto',
                        width: 500,
                        height: 333,
                        'z-index': 10,
                        position: 'fixed',
                        top: '10px',
                        left: '10px',
                        display: 'block',
                        background: '#BFBFBF',
                        opacity: '.7',
                        '-moz-border-radius': '1em',
                        border: 'grey solid 1px'
                    }
                }).inject($(document).getElement('body'));
            }
            new Element('button', {
                html: 'close',
                'class': 'DeleteButt',
                events: {
                    click: function() {
                        $('UCPANG:exifdata').dispose();
                    }
                }
            }).inject(exifDataDiv);
            exifDataTable = new Element('table', { border: 0 }).inject(exifDataDiv);
            exifDataTable.adopt(
                new Element('tr').adopt(
                    new Element('th', {
                        html: 'label'
                    }),
                    new Element('th', {
                        html: 'value'
                    }),
                    new Element('th', {
                        html: 'clean'
                    })
                )
            );
    for (var exifIdx = 0, exifLen = exifData.photo.exif.length; exifIdx < exifLen; ++exifIdx) {
                try {
                    var exif = exifData.photo.exif[exifIdx];
                    var inBold = $chk(exif.clean) || 
                            exif.tag === 'DateTimeOriginal' ||
                            exif.tag === 'CreateDate';
                    exifDataTable.adopt(
                        new Element('tr').adopt(
                            new Element('td', {
                                html: exif.label,
                                styles: {
                                    'font-weight': inBold ? 'bold' : 'normal'
                                }
                            }),
                            new Element('td', {
                                html: exif.raw._content,
                                styles: {
                                    'font-weight': inBold ? 'bold' : 'normal'
                                }
                            }),
                            new Element('td', {
                                html: ($chk(exif.clean) ? exif.clean._content : ' '),
                                styles: {
                                    'font-weight': inBold ? 'bold' : 'normal'
                                }
                            })
                        )
                    );
                } catch (e) {
                    GM_log("error: " + e);
                }
            }
            new Element('button', {
                html: 'close',
                'class': 'DeleteButt',
                events: {
                    click: function() {
                        $('UCPANG:exifdata').dispose();
                    }
                }
            }).inject(exifDataDiv);
        };
        var favoritesCallback = function (favoritesCell, data) {
            var favs = data.photo.total;
            if ($chk(favs)) {
                favoritesCell.adopt(document.createTextNode(favs + "f"));
            } else {
                favoritesCell.adopt(document.createTextNode('0f'));
            }
            favoritesCell.title = 'favs';
        };
        var poolCallback = function (poolCell, contexts) {
            if (contexts && contexts.pool) {
                var poolList = contexts.pool;
                for (var poolIdx = 0, nPools = poolList.length; poolIdx < nPools; ++poolIdx) {
                    var pool = poolList[poolIdx];
                    if (pool.id === groupConfig.groupId()) {
                        poolCell.adopt(new Element('img', {
                                src: "http://l.yimg.com/g/images/icon_check_small.png",
                                title: "photo is in the pool"
                            })
                        );
                        return;
                    }
                }
            }
            poolCell.adopt(new Element('img', {
                    src: errorIcon,
                    height: "12",
                    title: "photo is not in the pool"
                })
            );
        };
        basicButtons = addCheckButton([ takenCell, publicCell, canCommentCell, nViewsCell, nCommentsCell ],
            [ "taken", "public", "edit", "views", "comments" ],
            'flickr.photos.getInfo',
            { photo_id: photoId },
            [ takenDateCallback, isPublicCallback, canCommentCallback, nViewsCallback, nCommentsCallback ]
            );
        var exifButton = addCheckButton(
            [ exifCell ], 
            [ "EXIF" ], 
            'flickr.photos.getExif', 
            { photo_id: photoId }, 
            [ exifCallback ]
            );

        var favoritesButton = addCheckButton([ favoritesCell ], 
            [ "favs" ], 
            'flickr.photos.getFavorites', 
            { photo_id: photoId }, 
            [ favoritesCallback ]
            );

        var poolButton = addCheckButton([ poolCell ], 
            [ "pool" ], 
            'flickr.photos.getAllContexts', 
            {
                photo_id: photoId
            },
            [ poolCallback ]
        );
        //allAwardsCell.align = "right";
        var allAwardsButton = addAwardsCombo(allAwardsCell, 
            "awards in group", 
            awardsCell,
            groupPreferences.defaultAwardsGroup(),
            'flickr.photos.comments.getList', 
            { photo_id: photoId },
            allAwardsCallback
        );
        new Element('td', {
            colspan: 7,
            id: "UCPANG:" + photoId + ".cell",
            align: 'right'
        }).inject(exifCell, 'after');
        return photoId;
    }

    function drawPhotoInformationBoxes(challengeDefinition, challengeAnnouncement, onlyNonApproved) {
        var photoReplies = [];
	
        if (ucpThread.groupConfig().allowsPhotoInAnnouncement()) {
            photoReplies.extend(ucpFindPhotos(challengeAnnouncement, ucpThread, true));
        }
        // replies
        discussionTopic.getElements('table.TopicReply td.Said').each(function(node) {
            photoReplies.extend(ucpFindPhotos(node, ucpThread, true));
        });
        var photoId;
        // decorate photos with information
        photoReplies.each( function (ucpCompetitor, photoIdx) {
                if (!$chk(ucpCompetitor)) {
                    return;
                }
                var topic = ucpThread.topic();
                var photoRow = ucpCompetitor.node().getParent('tr');
                var photoSrc = ucpCompetitor.photo().get('src');
                var photoTextNode = photoRow.getElement('td.Said');
/*                if (!$chk(photoTextNode)) { // the thumbnail
                    return;
                } */
                var approvedNode = photoTextNode.getElement('img[alt*=UCPAapproved]');
                if ($chk(approvedNode) && onlyNonApproved) {
                    return;
                }
                var tmpPhotoId = drawPhotoInformationBox(ucpCompetitor);
                if ($chk(tmpPhotoId)) {
                    photoId = tmpPhotoId;
                } else {
                    return;
                }
                var approveButton = new Element('button', {
                    'class': 'Butt approve-button',
                    id: "UCPANG:" + photoId + ".approvebutton",
                    html: 'Approve',
                    title: "Mark as approved",
                    events: {
                        click: function (evt) {
			    var approveButton = $(evt.target);
                            // strip photoId
                            var photoId = approveButton.id.replace('UCPANG:', '').replace('.approvebutton', '');
                            var button = $('UCPANG:' + photoId + '.button');
                            var buttonCell = $('UCPANG:' + photoId + '.cell');
                            var infoBoxRow = $('UCPANG:' + photoId + '.row');
                            var photoTextNode = infoBoxRow.getPrevious('tr').getElement('td.Said');
                            buttonCell.empty();
                            var challengeNode = photoTextNode.clone(true, true); // copy children, copy id's
                            buttonCell.adopt(messageSpan = new Element('span'));
                            buttonCell.adopt(spin = new Element('img', {
                                src: updatingIcon
                            }));
                            // read current content of photoNode
                            //var content = challengeAnnouncement.innerHTML;
                            // remove all UCPapprovals, and <small> elements
                            // but first, get the edit link
                            var editLink = photoTextNode.getElement('small a[href*=edit]');
                            if (!$chk(editLink)) {
                                messageSpan.style.color = "red";
                                messageSpan.set('html', 'ERROR fetching edit link; aborting');
                                spin.dispose();
                                return;
                            }
                            // work with the clone from here on
                            challengeNode.getElements('img[alt*=UCPAapproved], p[class*=UCPANG], div.ucpdiv').dispose();
                            // more cleanup: trim left and right 
                            // adding to a div.innerHTML creates extra spaces and line breaks
                            //
			    var approvedNodeHtml = stripChallengeAnnouncement(challengeNode);

                            var approval = "<img " +  // seems not to work with a created element
                                   "src='http://l.yimg.com/g/images/spaceout.gif' " +
                                   "alt="; 
                            var username = GM_getLoggedInUser().replace(/:/g, '_')
                                                               .replace(/\'/g, '&#39;');
                            //[ ignore, name, photoChecksum, checksum, version ] = 
                            approval += "'UCPAapproved:" + 
                                encodeURIComponent(username) + ":" + 
                                ucpUniversalHash(photoSrc) + ":" +
                                ucpUniversalHash(photoId + username + topic) + ":" + 
                                "1' />"; // append 'version'
                            // post: based on the ideas found in 'Inline Comment Editor' by steeev 
                            // (http://www.flickr.com/groups/flickrhacks/discuss/72157600231591623/)
			    approvedNodeHtml += "\n<i>[approved by " + GM_getLoggedInUser() + "]</i>" + approval;
			    
                            // need to send subject?
                            if (groupConfig.allowsPhotoInAnnouncement() && 
                                !$chk(challengeNode.getParent('table.TopicReply'))) { // announcement
                                
                                var subject = $('GoodStuff').getElement('h2').get('html');
                            }
                
			    var message = approvedNodeHtml
                            var hostname = window.location.hostname;
                            var editLinkHref = editLink.href;
                            var apiUrl = editLinkHref;
                            messageSpan.adopt(document.createTextNode("sending updates to Flickr, please wait.."));
                            new Request({
                                method: "post",
                                url: apiUrl,
                                headers: {
                                    "Content-Type": "application/x-www-form-urlencoded",
                                    "Referer": apiUrl
                                },
                                data: "magic_cookie=" + GM_getAuthHash() + 
                                    "&done=1" +
                                    "&message=" + encodeURIComponent(message) +
                                    ($chk(subject) ? "&subject=" + encodeURIComponent(subject) : ""),
                                onFailure: function (response) {
                                    buttonCell.empty();
                                    buttonCell.adopt(new Element('img', { 
                                                            src: errorIcon,
							    title: response.statusText
                                                        }),
                                                     document.createTextNode("Failed: " + response.statusText)
                                                    );
                                },
                                onSuccess: function (responseText, responseXML) {
                                    var tempDiv = new Element('div');
                                    tempDiv.innerHTML = responseText.replace(/<script(.|\s)*?\/script>/g, '');
                                    var problem = tempDiv.getElement('p.Problem');
                                    if ($chk(problem)) {
                                        buttonCell.empty();
                                        buttonCell.adopt(
                                            new Element('img', { 
                                                src: errorIcon
                                            }),
                                            document.createTextNode("Failed: " + problem.get('html'))
                                        );
                                        return;
                                    }
                                    buttonCell.empty();
                                    buttonCell.adopt(
                                        new Element('img', { 
                                            src: "http://l.yimg.com/g/images/icon_check_small.png" 
                                        }),
                                        document.createTextNode("Approved")
                                    );
				    // cooperate with the 'workflow'
                                    var workflowLink = $('UCPANG-workflow-wizard-photo-approved-' + photoId);
                                    if ($chk(workflowLink)) {
                                       workflowLink.set('html',  workflowLink.get('html').replace(' (not formally approved)', ' approved by ' + GM_getLoggedInUser()));
                                       workflowLink.setStyle('color', '');
                                    }
				    var hiddenApproval = $('UCPANG-workflow-wizard-hidden-photo-approved-' + photoId);
				    if (!$chk(hiddenApproval)) {
					new Element('input', {
						type: 'hidden',
						id: 'UCPANG-workflow-wizard-hidden-photo-approved-' + photoId,
						value: true,
						alt: username
					}).inject($$('body')[0]);
				    } else {
				    	hiddenApproval.set('value', true);
				    	hiddenApproval.set('alt', username);
				    }
                                }
                            }).send();
                        }
                    }
                });
    
                var approvalCell = $('UCPANG:' + photoId + '.cell');
                if (!approvalCell) {
                    return;
                }
                if (!approvedNode) {
                    approvalCell.adopt(approveButton);
                    approvalCell.adopt(document.createTextNode(" "));
                } else { // already approved by an UCPA admin/mod
                    var approvalCheck = ucpCheckPhotoApproval(ucpCompetitor.photo(), ucpThread.topic());
                // returns
                // {
                //      approved: true/false,
                //      approver: string,
                //      version: number,
                //      checksum: true/false,
                //      photoChecksum: true/false,
                //      error: string
                //      photoId: string
                //  }
                    if (approvalCheck.approved) {
                        approvalCell.adopt(document.createTextNode("approved by " + approvalCheck.approver + " "));
                    } else if (!approvalCheck.checkSum) {
                        approvalCell.adopt(document.createTextNode("checksum is incorrect! "));
                        approvalCell.adopt(approveButton);
                        approvalCell.adopt(document.createTextNode(" "));
                    } else if (!approvalCheck.photoCheckSum) {
                        approvalCell.adopt(document.createTextNode("photo has been changed after approval! "));
                        approvalCell.adopt(approveButton);
                        approvalCell.adopt(document.createTextNode(" "));
                    } else if (approvalCheck.ignored) {
                        approvalCell.adopt(document.createTextNode(
                                "non competing image (by " + approvalCheck.approver + ") "));
                        approvalCell.adopt(approveButton);
                    } else {
                        approvalCell.style.color = 'red';
                        approvalCell.adopt(document.createTextNode("failed to process UCPA approved entry"));
                        approvalCell.adopt(approveButton);
                        approvalCell.adopt(document.createTextNode(" "));
                    }
                }
            }); // photo loop
            if (!$chk(photoId)) {
                return;
            }
            // redraw the last
            $('UCPANG:'+photoId+'.table').style.display = 'none';
            $('UCPANG:'+photoId+'.table').offsetHeight;
            $('UCPANG:'+photoId+'.table').style.display = 'block';

            allAllCell.adopt(
                $chk($('checkAllPhotosButton')) ? document.createTextNode("") : // smartPhotoBoxes
                new Element('button', {
                    'class': "Butt",
                    id: 'checkAllPhotosButton',
                    html: "check all photos",
                    events: {
                        'click': function () {
                            this.empty();
                            new Element('img', {
                                src: updatingIcon,
                            }).inject(this);
                            $$('button.check-all-button').each(function (button) {
                                button.click();
                            });
                            this.set('html', 'results are below each photo');
                            this.set('enabled', false);
                        },
                        'mouseover': function () {
                            $('ucpng_overrides_title_panel').set('html', "run all checks on all photos");
                        },
                        'mouseout': function () {
                            $('ucpng_overrides_title_panel').empty();
                        }
                    }
                }),
                document.createTextNode("  "),
                $chk($('approveAllPhotosButton')) ? document.createTextNode("") : // smartPhotoBoxes!
                new Element('button', {
                    'class': 'Butt approve-all-button',
                    id: 'approveAllPhotosButton',
                    html: "approve all photos",
                    events: {
                        'click': function (evt) {
                            var approveAllButton = $(evt.target);
			    approveAllButton.empty();
                            new Element('img', {
                                src: updatingIcon,
                            }).inject(approveAllButton);
                            $$('button.approve-button').each(function (button) {
                                button.click();
                            });
                            approveAllButton.dispose();
                        },
                        'mouseover': function () {
                            $('ucpng_overrides_title_panel').set('html', "approve all photos");
                        },
                        'mouseout': function () {
                            $('ucpng_overrides_title_panel').empty();
                        }
                    }
                })
            );
    }

    function togglePhotoInformationBoxes(challengeDefinition, challengeAnnouncement) {
        var allAllCell = $('allAllCell');
        var showInfoBoxesCheckbox = $('showInfoBoxes');
        if ($chk(showInfoBoxesCheckbox) && !showInfoBoxesCheckbox.checked) {
            // clean up
            if ($chk($('checkAllPhotosButton'))) {
                $('checkAllPhotosButton').dispose();
            }
            if ($chk($('approveAllPhotosButton'))) {
                $('approveAllPhotosButton').dispose();
            }
            $$('table[id^=UCPANG:][id$=.table]').getParent().dispose();
        } else {
            drawPhotoInformationBoxes(challengeDefinition, challengeAnnouncement, false);
        }
    }

    var UCPAVoteOverride = new Class({
        Implements: [Options],
        options: {
            vote: undefined,
            uiElement: undefined,
            id: undefined
        },
        initialize: function (options) {
            this.setOptions(options);
            this.options.id = this.options.uiElement.getParent('td.Said').
                                                     getParent('tr').
                                                     getElement('td.Who').
                                                     getElement('a').get('name');
        },
        handleEvent: function (e) {
            this.options.uiElement.empty();
            var div = new Element('div', {
                id: "UCPANG:replyOverrideDialogDiv:" + this.options.id,
                styles: ucpDialogStyle
            }).inject(this.options.uiElement);
            var table = new Element('table', {
                border: '0',
                cellPadding: '5',
                cellSpacing: '0'
            }).inject(div);
            table.adopt(
                new Element('tr').adopt(
                    new Element('td', {
                        html: this.options.vote.poster().username
                    })
                )
            );
        }
    });

    var UCPACommentOverride = new Class({
        Implements: [Options],
        options: {
            comment: undefined,
            uiElement: undefined,
            id: undefined
        },
        initialize: function (options) {
            this.setOptions(options);
            this.options.id = this.options.uiElement.getParent('td.Said').
                                                     getParent('tr').
                                                     getElement('td.Who').
                                                     getElement('a').get('name');
        },
        handleEvent: function (e) {
            this.options.uiElement.empty();
            var div = new Element('div', {
                id: "UCPANG:replyOverrideDialogDiv:" + this.options.id,
                styles: ucpDialogStyle
            }).inject(this.options.uiElement);
            var table = new Element('table', {
                border: '0',
                cellPadding: '5',
                cellSpacing: '0'
            }).inject(div);
            table.adopt(
                new Element('tr').adopt(
                    new Element('td', {
                        html: this.options.node.poster().username
                    })
                )
            );
        }
    });

    function processDiscussionTopicInList(discussionTopic, ucpThread) {
        var infoCell = $('UCPANG.info.' + ucpThread.topic());
        if (!$chk(infoCell)) {
            GM_log("no infoP!!");
            return;
        }
        var threadInfoImg = infoCell.getParent('td').getElement('input[type=image]');
        threadInfoImg.set('src', images.up); // replace spinning balls
        infoCell.empty();
        try {
            var challengeAnnouncementRow = $(discussionTopic).getElement('td.Said').getParent('tr');
        } catch (e) {
            GM_log("error: " + e);
            // ignore
        }
        if (!$chk(challengeAnnouncementRow) || challengeAnnouncementRow.lenght === 0) {
            GM_log("no announcement");
            ucpThread.setChallengeStatus("ERRORPARSING");
            ucpThread.store();
            ucpThread.printStatus();
            return "none";
        }
        var challengeDefinition = ucpThread.challengeDefinition();
        var challengeAnnouncementCell = challengeAnnouncementRow.getElement('td.Said'); // overrides: use the 'td' element
        var overridesDefined = challengeDefinition.readChallengeDefinitionOverrides(challengeAnnouncementCell);
        if (overridesDefined) {
            threadInfoImg.style.background = 'yellow';
        }
        try {
            var okImg = new Element('img', {
                src: defaultCheckIconSmall
            });
            var failImg = new Element('img', {
                src: errorIcon
            });
            if (challengeDefinition.scoreType() === "UNKNOWN") {
                infoCell.adopt(document.createTextNode('could not find a match'));
                infoCell.adopt(failImg.clone());
                threadInfoImg.style.background = 'red';
            } else {
                infoCell.adopt(
                    new Element('i').adopt(
                        new Element('span', {
                            html: challengeDefinition.name()
                        })
                    ),
                    new Element('span', {
                        html: " (score type "
                    }),
                    new Element('i').adopt(
                        new Element('span', {
                            html: challengeDefinition.scoreType()
                        })
                    ),
                    document.createTextNode(") "),
                    okImg.clone()
                );
            }
            // state check
            if (challengeDefinition.nonChallengeType()) {
                // no state to check
            } else { // (nameOk && isChallenge) || !nameOk
                infoCell.adopt(new Element('span', {
                    html: " --- "
                    })
                );
                // sometime, in some groups, normal challenges are numbered, closed ones not anymore
                var stateOk = false;
                var state;
                for (state in groupConfig.states()) {
                    if (!state.match(/RegExp$/) && groupConfig.states().hasOwnProperty(state)) {
                        if (ucpThread.challengeName().match(groupConfig.states()[state])) {
                            stateOk = true;
                            break;
                        }
                    }
                }
                if (stateOk) {
                    infoCell.adopt(
                        new Element('span', {
                            html: "status ("
                        }),
                        new Element('i').adopt(
                            new Element('span', {
                                html: state
                            })
                        ),
                        new Element('span', {
                            html: ") "
                        }),
                        okImg.clone()
                    );
                    if (challengeDefinition.scoreType() === "UNKNOWN") { // infoImg has red background
                        threadInfoImg.style.background = 'orange';
                    }
                } else {
                    infoCell.adopt(
                        document.createTextNode("no matching status found "),
                        failImg.clone()
                    );
                    threadInfoImg.style.background = 'red';
                }
            }
        } catch (e) {
            GM_log("error: " + e);
        }
        if (ucpThread.challengeDefinition().nonChallengeType()) {
            return;
        }
        ucpThread.printStatus(); // TODO: this one should do what's been done above
        var challengeEntries = $(discussionTopic).getElements('td.Said');
        
        if (groupConfig.excludeReplyIndexes().contains(0)) {
            ucpThread.findExcludesInDOMNode(challengeAnnouncementCell);
        }
        groupConfig.excludeReplyIndexes().each(function (index) {
            var challengeEntry = challengeEntries[index];
            if (challengeEntry && index > 0) { // already done the announcement
                ucpThread.findExcludesInDOMNode(challengeEntry);
            }
        });
        //GM_log("ready to print excludes");
        ucpThread.printExcludes(challengeAnnouncementCell);

        ucpThread.collectVotes(challengeAnnouncementCell, challengeEntries);
        var votes  = ucpThread.votes();
        var comments = ucpThread.comments();
        var photos = ucpThread.photos();
        infoCell.adopt(
            new Element('br'),
            new Element('b', {
                html: 'photos:'
            })
        );
        photos.each(function (photo) {
            try {
                photo.printStatus(ucpThread.replyDecoratorFactory());
            } catch (e) {
                GM_log("error: " + e);
            }
        });
        infoCell.adopt(
            new Element('br'),
            new Element('b', {
                html: 'votes:'
            })
        );
        try {
            ucpProcessVotes(ucpThread);
        } catch (e) {
            GM_log("error processing votes: " + e);
        }
    }

    function processDiscussionTopic(discussionTopic, ucpThread, adminOrMod) {
        // announcement overrides
        try {
            var challengeAnnouncementCell = discussionTopic.getElement('td.Said');
            var challengeAnnouncementRow = challengeAnnouncementCell.getParent('tr');
        } catch (e) {
            // ignore
        }
        if (!$chk(challengeAnnouncementRow) || challengeAnnouncementRow.lenght === 0) {
            ucpThread.setChallengeStatus("ERRORPARSING");
            ucpThread.store();
            return "none";
        }
        (new Element('tr').adopt(
            new Element('td', {
                colspan: 2
            }).adopt(
                ucpNewCell = new Element('p', { 
                    border: '0', 
                    styles: ucpDialogStyle 
                })
            ))
        ).inject(challengeAnnouncementRow, 'after');
        var challengeDefinition = ucpThread.challengeDefinition();
        var challengeAnnouncementWithBlockquote = $chk(challengeAnnouncementCell.getElement('blockquote'));
        var challengeAnnouncement = challengeAnnouncementCell;//.getElement('p');
        // reading overrides: we can use the 'td' element
        challengeDefinition.readChallengeDefinitionOverrides(challengeAnnouncement);
        // print challenge config summary
        var challengeDefinitionTable = new UCPChallengeDefinitionUI(challengeDefinition.originalChallengeDefinition(), ucpThread.challengeName())
                                        .printAsTableWithDiff(
                                            new UCPChallengeDefinitionUI(challengeDefinition, ucpThread.challengeName()), 
                                            challengeAnnouncement, 
                                            ucpNewCell, 
                                            adminOrMod.moderator || adminOrMod.administrator);
        if (ucpThread.challengeDefinition().nonChallengeType() || ! (adminOrMod.moderator || adminOrMod.administrator ) ) {
            return;
        }
        challengeDefinitionTable.adopt(allAllRow = new Element('tr'));
        allAllRow.adopt(allAllCell = new Element('td', {
            colSpan: 10,
            id: 'allAllCell'
        }));
        allAllCell.adopt(
            allAllCheck = new Element('input', {
                type: 'checkbox',
                id: 'showInfoBoxes',
                events: {
                    click: function () {
                        togglePhotoInformationBoxes(challengeDefinition, challengeAnnouncementCell);
                    }
                }
            }),
            new Element('label', {
                html: 'show photo information boxes ',
                'for': 'showInfoBoxes'
            })
        );
        if (groupPreferences.smartPhotoBoxes()) {
            drawPhotoInformationBoxes(challengeDefinition, challengeAnnouncementCell, true);
        }
    }    

    function newNodeHandler(evt) {
        var target = $(evt.target);
        if (target.nodeName == 'DIV') {
		var div = $(target);
		if (div.get('id') == 'ined_edit') {
			// disable the workflow's SAVE button
			var saveButton = $('UCPANG-promote-wizard-save-button');
			if ($chk(saveButton)) {
				saveButton.set('class', 'DisabledButt');
				saveButton.set('title', 'close the inline edit box');
			}
		}
	}
    }
    function removedNodeHandler(evt) {
	var target = $(evt.target);
	if (target.nodeName == 'DIV') {
		var tarea = $(target);
		if (tarea.get('id') == 'ined_edit') {
			var saveButton = $('UCPANG-promote-wizard-save-button');
			if ($chk(saveButton)) {
				saveButton.set('title', '');
				if ($$('img.UCPANG-workflow-wizard-save-button').length == 0) {
					$('UCPANG-promote-wizard-save-button').set('class', 'Butt');
				}
			}
		}
	}
    }

    function addWorkflowDialog(discussionTopic, ucpThread) {
        (new Element('tr').adopt(
            new Element('td', {
                colspan: 2
            }).adopt(
                ucpWorkflowCell = new Element('p', { 
                    border: '0', 
		    id: 'UCPANG-workflow-cell',
                    styles: ucpDialogStyle 
                })
            ))
        ).inject($chk($('UCPANG-bump-button')) ? $('UCPANG-bump-button') : $('GoodStuff').getElement('h2'), 'after');
        var votes  = ucpThread.votes();
        var comments = ucpThread.comments();
        var photos = ucpThread.photos();
	photos.forEach( function (photo) {
		if (!$chk($('UCPANG-workflow-wizard-hidden-photo-approved-' + photo.photoId()))) {
		    new Element('input', { 
			type: 'hidden', 
			id: 'UCPANG-workflow-wizard-hidden-photo-approved-' + photo.photoId(), 
			value: photo.approved(),
			alt: photo.approvedBy()
		    }).inject($$('body')[0]);
		}
	});
	ucpWorkflowCell.adopt(new Element('p', {
		html: 'UCP workflow:',
		styles: {
			fontWeight: 'bold'
		}
	}));
        var challengeDefinition = ucpThread.challengeDefinition();
	ucpWorkflowCell.adopt(new Element('div', {
		html: 'found ' + photos.length + ' photo' + (photos.length == 1 ? '' : 's') + 
		      (challengeDefinition.neededPhotos() > 0 ? ' (' + challengeDefinition.neededPhotos() + ' needed)' : '')
	}));
	if (votes.length > 0) {
		var lastvote = votes[votes.length - 1];
		ucpWorkflowCell.adopt(new Element('div', {
			html: 'score: <b>' + ucpThread.scoreSummary(false) + '</b> (last vote by ' + lastvote.poster().username + ')'
		}));
	}
	ucpWorkflowCell.adopt(new Element('div', {
		html: 'status: ' + ucpThread.challengeStatus()
	}));
	if (ucpThread.hasError()) {
		new Element('div', {
			html: 'error(s): ' + ucpThread.error(),
			styles: {
				color: 'red'
			}
		}).inject(ucpWorkflowCell);
	}
	if (ucpThread.hasWarning()) {
		new Element('div', {
			html: 'warning(s): ' + ucpThread.warning(),
			styles: {
				color: 'red'
			}
		}).inject(ucpWorkflowCell);
	}
	switch (ucpThread.challengeStatus()) {
		case "Open":
			provideOpenToVoteWizard(ucpThread.filled(), ucpWorkflowCell);
			provideVoteToClosedWizard(false, ucpThread, ucpWorkflowCell);
			challengeDefinition.workflow().recycle_thread == true ? 
				provideClosedToCleaningWizard(false, ucpThread, ucpWorkflowCell) :
				provideClosedToAwardedWizard(false, ucpThread, ucpWorkflowCell);
			provideAwardedToNewWizard(false, ucpThread, ucpWorkflowCell);
			break;
		case "Filled":
			provideOpenToVoteWizard(true, ucpWorkflowCell);
			provideVoteToClosedWizard(false, ucpThread, ucpWorkflowCell);
			challengeDefinition.workflow().recycle_thread == true ? 
				provideClosedToCleaningWizard(false, ucpThread, ucpWorkflowCell) :
				provideClosedToAwardedWizard(false, ucpThread, ucpWorkflowCell);
			provideAwardedToNewWizard(false, ucpThread, ucpWorkflowCell);
			break;
		case "--VOTE--":
			provideOpenToVoteWizard(false, ucpWorkflowCell);
			provideVoteToClosedWizard(challengeDefinition.workflow().recycle_thread ? false : ucpThread.finished(), ucpThread, ucpWorkflowCell);
			challengeDefinition.workflow().recycle_thread == true ? 
				provideClosedToCleaningWizard(false, ucpThread, ucpWorkflowCell) :
				provideClosedToAwardedWizard(false, ucpThread, ucpWorkflowCell);
			provideAwardedToNewWizard(false, ucpThread, ucpWorkflowCell);
			break;
		case "Finished":
			provideOpenToVoteWizard(false, ucpWorkflowCell);
			provideVoteToClosedWizard(challengeDefinition.workflow().recycle_thread ? false : !ucpThread.isClosed().closed, ucpThread, ucpWorkflowCell);
			challengeDefinition.workflow().recycle_thread == true ? 
				provideClosedToCleaningWizard(ucpThread.finished(), ucpThread, ucpWorkflowCell) :
				provideClosedToAwardedWizard(ucpThread.isClosed().closed && ucpThread.isClosed().closer.innerHTML.match(GM_getLoggedInUser()), ucpThread, ucpWorkflowCell);
			provideAwardedToNewWizard(false, ucpThread, ucpWorkflowCell);
			break;
		case "Closed":
			provideOpenToVoteWizard(false, ucpWorkflowCell);
			provideVoteToClosedWizard(false, ucpThread, ucpWorkflowCell);
			challengeDefinition.workflow().recycle_thread == true ? 
				provideClosedToCleaningWizard(false, ucpThread, ucpWorkflowCell) :
				provideClosedToAwardedWizard(false, ucpThread, ucpWorkflowCell);
			provideAwardedToNewWizard(ucpThread.isClosed().closed && ucpThread.isClosed().closer.innerHTML.match(GM_getLoggedInUser()), ucpThread, ucpWorkflowCell);
			break;
		case "Unknown":
			provideAwardedToNewWizard(false, ucpThread, ucpWorkflowCell);
			break;
		case "none":
		default:
			new Element('div', {
				html: 'error: was unable to calculate challenge status - please report (state=\'' + ucpThread.challengeStatus() + '\''
			}).inject(ucpWorkflowCell);
			return; // leave
	}

    }

    function provideWorkflowAction(enabled, ucpWorkflowCell, idInfix, label, buttonText, toggleWizardCallback) {
        var elementDiv = new Element('div').inject(ucpWorkflowCell);
	new Element('input', {
            type: 'radio',
            id: 'UCPANG-' + idInfix + 'Wizard-checkbox',
	    name: 'UCPANG-workflow-action',
            checked: enabled,
	    events: {
		'change': function (evt) {
			$$('label[id*=Wizard-label]').forEach(function (el) {
				el.setStyle('color', 'grey');
			});
			$$('button[id*=Wizard-button]').forEach(function (el) {
				el.set('class', 'DisabledButt');
			});
			$('UCPANG-' + idInfix + 'Wizard-label').setStyle('color', '');
			$('UCPANG-' + idInfix + 'Wizard-button').set('class', 'Butt');
			}
		}
        }).inject(elementDiv);
	new Element('label', {
		'for': 'UCPANG-' + idInfix + 'Wizard-checkbox',
		id: 'UCPANG-' + idInfix + 'Wizard-label',
		html: label,
		styles: {
		    color: enabled ? '' : 'grey'
		}
	}).inject(elementDiv);
	new Element('span', { html: ' ' }).inject(elementDiv);
	new Element('button', {
		'class': enabled ? 'Butt' : 'DisabledButt',
		id: 'UCPANG-' + idInfix + 'Wizard-button',
		html: buttonText,
		title: 'opens the \'' + buttonText + '\' dialog',
		events: {
			'click': function () {
				if ( ! $('UCPANG-' + idInfix + 'Wizard-checkbox').checked) {
					return;
				}
				toggleWizardCallback(ucpThread);
			}
		}
	}) .inject(elementDiv);
	new Element('span', { html: ' ' }).inject(elementDiv);
    }
    
    function provideOpenToVoteWizard(enabled, ucpWorkflowCell) {
	provideWorkflowAction(enabled, ucpWorkflowCell, 
		'openToVote', 
		'1. change challenge title from state \'open\' to state \'vote\'', 
		'Promote..', 
		toggleOpenToVoteWizardDialog);
    }

    function provideVoteToClosedWizard(enabled, ucpThread, ucpWorkflowCell) {
    	provideWorkflowAction(enabled, ucpWorkflowCell,
		'voteToClosed',
		'2. close the challenge thread',
		'Claim ownership..',
		toggleVoteToClosedWizardDialog);
    }

    function provideClosedToCleaningWizard(enabled, ucpThread, ucpWorkflowCell) {
    	provideWorkflowAction(enabled, ucpWorkflowCell,
		'closedToCleaning',
		'3. clean challenge (remove votes, add losing thumbnail, ..)',
		'Clean..',
		toggleClosedToCleaningWizardDialog);
    }

    function provideClosedToAwardedWizard(enabled, ucpThread, ucpWorkflowCell) {
    	provideWorkflowAction(enabled, ucpWorkflowCell,
		'closedToAwarded',
		'3. finalize challenge (award, tag, invite..)',
		'Finalize..',
		toggleClosedToAwardedWizardDialog);
    }

    function provideAwardedToNewWizard(enabled, ucpThread, ucpWorkflowCell) {
    	provideWorkflowAction(enabled, ucpWorkflowCell,
                'awardedToNew',
		'4. start a new challenge',
		'Create..',
		toggleAwardedToNewWizardDialog);
    }

    function createEditableLabelElement(parent, id, defaultValue, validateFunction) {
    	    var newValueValidation = {
	    	color: 'black',
		valid: true,
		error: undefined
	    };
	    if ($chk(validateFunction)) {
	    	newValueValidation = validateFunction(defaultValue);
	    }
	    new Element('b', {
		html: defaultValue,
		id: id,
		styles: {
			color: newValueValidation.color
		},
		title: newValueValidation.title,
		events: {
			'update-theme': function (evt) {
					var newTheme = evt;
					var placeholder = '%__theme__%';
					this.set('html', $(id + '-hidden').get('value').replace(placeholder, newTheme));
				}
		}
	    }).inject(parent);
	    new Element('input', {
	    	type: 'hidden',
		id: id + '-hidden', // contains the templated version (%__theme__% or %__username__%)
		value: defaultValue
	    }).inject(parent);
	    new Element('span', {
	    	html: '    '
	    }).inject(parent);
	    new Element('img', {
		src: images.edit,
		id: id + '-edit-button',
		height: 16,
		title: 'edit',
		'class': 'UCPANG-workflow-wizard-edit-button',
		styles: {
			cursor: 'pointer'
		},
		events: {
			'click': function (evt) {
				var editImg = $(evt.target);
				var id = editImg.id.replace('-edit-button', '');
				if (editImg.hasClass('UCPANG-workflow-wizard-edit-button')) {
					$('UCPANG-promote-wizard-save-button').set('class', 'DisabledButt');
					$('UCPANG-promote-wizard-save-button').set('title', 'first close any edit fields');
					$(id).set('style', 'visibility: hidden; display: none');
					editImg.set('src', images.save);
					editImg.set('title', 'save');
					editImg.set('class', 'UCPANG-workflow-wizard-save-button');
					var cell = $(evt.target).getParent();
					new Element('input', {
						type: 'text',
						id: id + '-input',
						value: $(id).get('text'), // not 'html': 'text' decodes html entities
						styles: {
							width: '80%'
						}
					}).inject(editImg, 'before');
				} else {
					$(id).set('style', 'visibility: visible; display: inline');
					editImg.set('src', images.edit);
					editImg.set('title', 'edit');
					editImg.set('class', 'UCPANG-workflow-wizard-edit-button');
					var editedTitle = $(id + '-input').value;
					$(id + '-input').dispose();
					$(id).set('html', editedTitle);
					var validation = newValueValidation;
					if ($chk(validateFunction)) validation = validateFunction(editedTitle);
					$(id).setStyle('color', validation.color);
					$(id).set('title', validation.title);
					if ($$('img.UCPANG-workflow-wizard-save-button').length == 0 && !$chk($('ined_edit'))) {
						$('UCPANG-promote-wizard-save-button').set('class', 'Butt');
						$('UCPANG-promote-wizard-save-button').set('title', '');
					}
				}
			}
		}
	    }).inject(parent);
    }

    function createEditableTextElement(parent, id, bgColor, defaultValue) {
	    new Element('div', {
		html: $chk(defaultValue) ? defaultValue : '<b>--none--</b>',
		id: id,
		styles: {
			border: '1px solid',
			width: '500px',
			backgroundColor: ($chk(bgColor) ? bgColor : '')
		}
	    }).inject(parent);
	    new Element('span', {
	    	html: '    '
	    }).inject(parent);
	    new Element('img', {
		src: images.edit,
		id: id + '-edit-button',
		height: 16,
		title: 'edit',
		'class': 'UCPANG-workflow-wizard-edit-button',
		styles: {
			cursor: 'pointer'
		},
		events: {
			'click': function (evt) {
				var editImg = $(evt.target);
				var id = editImg.id.replace('-edit-button', '');
				if (editImg.hasClass('UCPANG-workflow-wizard-edit-button')) {
					$('UCPANG-promote-wizard-save-button').set('class', 'DisabledButt');
					$('UCPANG-promote-wizard-save-button').set('title', 'first close any edit fields');
					$(id).setStyle('visibility', 'hidden');
					$(id).setStyle('display',  'none');
					editImg.set('src', images.save);
					editImg.set('class', 'UCPANG-workflow-wizard-save-button');
					var cell = $(evt.target).getParent();
					new Element('textarea', {
						rows: 20,
						cols: 80,
						id: id + '-input',
						value: $(id).get('html').replace(/\n/g, '').replace(/<br[^>]*>/g, '\n') // here we need the 'html': 'text' strips images
					}).inject(editImg, 'before');
				} else {
					$(id).setStyle('visibility', 'visible');
					$(id).setStyle('display', 'block');
					editImg.set('src', images.edit);
					editImg.set('class', 'UCPANG-workflow-wizard-edit-button');
					var editedText = $(id + '-input').value;
					$(id + '-input').dispose();
					$(id).set('html', editedText.replace(/\n/g, '<br>'));
					if ($$('img.UCPANG-workflow-wizard-save-button').length == 0 && !$chk($('ined_edit'))) {
						$('UCPANG-promote-wizard-save-button').set('class', 'Butt');
						$('UCPANG-promote-wizard-save-button').set('title', '');
					}
				}
			}
		}
	    }).inject(parent);
    }

    function provideTitleChangeRow(data) {
    	var table = data.table;
	var titleReplacer = data.titleReplacer;
	var nextStepChecker = data.nextStepChecker;
	
	var currentTitleRow = new Element('tr', { id: 'UCPANG-workflow-wizard-current-title-row' }).inject(table);
	var currentTitle = $('GoodStuff').getElement('h2').get('html');
	var currentTitleLabelCell = new Element('td', {
		html: 'current title:',
		id: 'UCPANG-workflow-wizard-current-title-label-cell'
	    }).inject(currentTitleRow);
	var currentTitleCell = new Element('td', {
		html: '<b>' + currentTitle + '</b>',
		colspan: 2, // 3 moves the titles too much to the rigth in the promote dialog (1 is also fine)
		id: 'UCPANG-workflow-wizard-current-title-cell'
	    }).inject(currentTitleRow);

	var newTitleRow = new Element('tr', { id: 'UCPANG-workflow-wizard-new-title-row' }).inject(table);
	var newTitleLabelCell = new Element('td', { html: 'new title:',
				id: 'UCPANG-workflow-wizard-new-title-label' }).inject(newTitleRow);
	var newTitleCell = new Element('td', {
				colspan: 3,
				id: 'UCPANG-workflow-wizard-new-title-cell' }).inject(newTitleRow);

	    var newTitle = currentTitle;
	    if (currentTitle.match(new RegExp(titleReplacer.expression, titleReplacer.flags))) {
	    	newTitle = currentTitle.replace(new RegExp(titleReplacer.expression, titleReplacer.flags),
			$chk(titleReplacer.template_replacement) ? titleReplacer.template_replacement :
			$chk(titleReplacer.replacement) ? titleReplacer.replacement :
							    new Function(titleReplacer.replacement_function));
	    } else if ($chk(titleReplacer.expression2) && currentTitle.match(new RegExp(titleReplacer.expression2, titleReplacer.flags2))) {
	    	newTitle = currentTitle.replace(new RegExp(titleReplacer.expression2, titleReplacer.flags2), 
			$chk(titleReplacer.replacement2) ? titleReplacer.replacement2 :
							     new Function(titleReplacer.replacement2_function)
			);
	    }

    	    createEditableLabelElement(newTitleCell, 'UCPANG-workflow-wizard-newtitle', newTitle, function (title) {
	    	if (title.match(new RegExp(nextStepChecker.expression, nextStepChecker.flags))) {
			return {
				color: 'black'
			}
		} else if ($chk(nextStepChecker.expression2) && title.match(new RegExp(nextStepChecker.expression2, nextStepChecker.flags2))) {
			return {
				color: 'black'
			}
		} else {
			return {
				color: 'red',
				title: 'this title will not be recognized in the next step'
			}
		}
	    });
	    return { currentTitle: currentTitle, newTitle: newTitle };
    }

    function toggleOpenToVoteWizardDialog() {
	if (SIMULATE) alert("simulating");
	if ($chk($('UCPANG-promote-wizard-dialog'))) {
		$('UCPANG-promote-wizard-dialog').destroy();
	} else {
            wizardDialog = new Element('table', {
		border: 0,
		id: 'UCPANG-promote-wizard-dialog',
                styles: ucpDialogStyle
	    });
	    var titles = provideTitleChangeRow({
	    	table: wizardDialog,
	    	titleReplacer: groupConfig.workflow().fromOpenToVoteRegExp,
	    	nextStepChecker: groupConfig.workflow().fromVoteToClosedRegExp
	    });
	    var newTitle = titles.newTitle;
	    var currentTitle = titles.currentTitle;

	    var photoRow = new Element('tr').inject(wizardDialog);
	    var photoCell = new Element('td', { colspan: 3 }).inject(photoRow);
	    var allApproved = true;
	    ucpThread.photos().forEach(function (photo) {
	    	try {
			var approved = $('UCPANG-workflow-wizard-hidden-photo-approved-' + photo.photoId()).get('value');
			approved = (approved == true || approved == 'true');
			allApproved = allApproved && approved;
			var approvedBy = $('UCPANG-workflow-wizard-hidden-photo-approved-' + photo.photoId()).get('alt');
		    	new Element('a', {
				html: 'photo by ' + photo.owner().username + (approved ? ' approved by ' + approvedBy : ' (not formally approved)'),
				href: '#' + photo.node().getParent('tr').getElement('td.Who').getElement('a').get('name'),
				id: 'UCPANG-workflow-wizard-photo-approved-' + photo.photoId(),
				styles: {
					color: approved ? '' : 'red'
				},
				events: {
					click: function(evt) {
						// we perform the scrolling ourselves:
						// we get the click event before the browser scrolls down
						var target = $(evt.target);
						evt.stop();
						// first, remove the dialog, because it adds to the height of the page
						wizardDialog = $('UCPANG-promote-wizard-dialog').dispose();
						// then, scroll down to the photo
						var name = target.get('href').replace('#', '');
						var position = $$('a[name=' + name + ']')[0].getParent('tr').getOffsets();
						if (position.y > 10) {
							$(window).scrollTo(0, position.y);
						}
						// place the dialog next to the photo
						var topline = 30;
						var float = new Element('div', {
					            styles: {
					                overflow: 'auto',
					                zIndex: 1014,
					                maxHeight: (window.innerHeight - topline - 20),
					                maxWidth: (window.innerWidth - 50),
					                minWidth: 640,
					                position: 'fixed',
					                opacity: '0.98',
					                left: '10px', //evt.event.pageX,
					                top: topline,
					                display: 'block',
					                visibility: 'visible'
					            }
					        }).inject($(document).getElement('body'));
						float.adopt(wizardDialog);
					}
				}
			}).inject(new Element('div').inject(photoCell));
		} catch (e) {
			GM_log("error listing approvals: " + e);
		}
	    });

	    var buttonRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    var buttonCell = new Element('td', {
		colspan: 2,
		align: 'right',
		valign: 'top'
	    }).inject(buttonRow);
	    if (!allApproved) {
            	new Element('button', {
                	'class': "Butt",
                	id: 'checkAllPhotosButton2',
                	html: "check all photos",
                	events: {
                    		'click': function () {
                        		this.empty();
                        		new Element('img', {
                            			src: updatingIcon,
                        		}).inject(this);
                        		$$('button.check-all-button').each(function (button) {
                            			button.click();
                        		});
                        		this.set('html', 'results are below each photo');
                        		this.set('enabled', false);
                    		},
                    		'mouseover': function () {
                        		$('ucpng_overrides_title_panel').set('html', "run all checks on all photos");
                    		},
                    		'mouseout': function () {
                        		$('ucpng_overrides_title_panel').empty();
                    		}
                	}
	    	}).inject(buttonCell);
	    	new Element('span', {
			html: ' '
	    	}).inject(buttonCell);
	    }
	    new Element('button', {
		'class': 'CancelButt',
		html: 'Cancel',
		events: {
			'click': function (evt) {
				$('UCPANG-promote-wizard-dialog').destroy();
			}
		}
	    }).inject(buttonCell);
	    new Element('span', {
		html: ' '
	    }).inject(buttonCell);
	    new Element('button', {
		'class': $chk($('ined_edit')) ? 'DisabledButt' : 'Butt', // prevent posting the edit box from Steeev's inline editor
		html: 'SAVE',
		id: 'UCPANG-promote-wizard-save-button',
		events: {
			'click': function (evt) {
				var saveButton = $('UCPANG-promote-wizard-save-button');
				if (saveButton.hasClass('Butt')) {
					saveButton.set('class', 'DisabledButt');
					updateChallengeTitle($('UCPANG-workflow-wizard-newtitle').get('text'), function (retval) { // use 'text', not 'html'
					    try {
						var threadURL = /(.*flickr.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(document.location.href)[1] + '/';
						new Element('a', {
							href: threadURL,
							html: 'click here to reload the page'
						}).inject(new Element('td', { colspan: 2 }).inject(new Element('tr').inject($('UCPANG-promote-wizard-dialog'))));
                                                if (retval.success) {
							saveButton.getElement('img').set('src', defaultCheckIconSmall);
                                                        document.location.href = threadURL;
                                                } else {
							saveButton.getElement('img').set('src', errorIcon);
							saveButton.set('title', retval.msg)
                                                        new Element('label', {
								html: retval.msg,
								styles: {
									color: 'red',
									weight: 'bold'
								}
							}).inject(new Element('td', { colspan: 2 }).inject(new Element('tr').inject($('UCPANG-promote-wizard-dialog'))));
                                                }
					    } catch (e) {
						alert(e);
					    }
                                        });
				}
			}
		}
	    }).inject(buttonCell);

	    wizardDialog.inject($('UCPANG-workflow-cell'), 'after');
	}
    }

    var loadedMedals = undefined; 
    function loadMedals(callback) {
    	var workflow = groupConfig.workflow();
	if (!$chk(workflow)) {
		loadedMedals = {
			success: false,
			error: 'no workflow for this group'
		};
		callback();
		return;
	}
	var medalThread = workflow.medalThread;
	if (!$chk(medalThread)) {
		loadedMedals = {
			success: false,
			error: 'no medal thread defined in workflow - contact Alesa Dam for support'
		};
		callback();
		return;
	}
	new Request({
                method:"GET",
                url:medalThread,
                headers:{
                        "User-Agent":"monkeyagent",
                        "Accept":"text/monkey,text/xml"
                },
                onSuccess:function(responseText, responseXML) {
          	    try {
                	var tempDiv = new Element('div', {
                    		html: responseText.stripScripts()
                	});

			if ($chk(tempDiv.getElement('p.Problem'))) {
				loadedMedals = {
					success: false,
					error: tempDiv.getElement('p.Problem').innerHTML
				};
				callback();
				return;
			}
			try {
            			var replies = tempDiv.getElement('div[id=DiscussTopic]').getElement('table.TopicReply').getElements('td.Said');
			} catch (e) {
				loadedMedals = {
					success: false,
					error: 'no replies found in medal thread'
				};
				callback();
				return;
			}
			loadedMedals = {
				success: true,
				tagsThread: undefined,
				inviteThread: undefined,
				sweepThread: undefined,
				winnerThread: undefined,
				medals: []
			};
            		replies.each(function (reply, idx) {
                		try {
                    			var title = reply.innerHTML.split("===Start of Title===")[1]
                                       				   .split("===End of Title===")[0]
                                       				   .replace(/<[^>]+>/g, '');
                    			var medal = reply.innerHTML.split("===Start of Medal===")[1]
                                              			   .split("===End of Medal===")[0]
								   .replace(/\n/g, '')
								   .replace(/^<br>/g, '')
                                               			   .replace(/<br>/g, '\n');
					if (reply.innerHTML.match("===Start of Tag===")) { // v1.6.2 and prior
	                    			var tags = reply.innerHTML.split("===Start of Tag===")[1]
                                               			  .split("===End of Tag===")[0]
                                               			  .replace(/<br>/g, '\n');
					}
					if (reply.innerHTML.match("===Start of Tags===")) { // 1.6.3 and newer
						/* => per medal
						tags: "challenge winner" test
						reportMissing: http://www.flickr.com/groups/some_backroom/discuss/987654321
						html: unable to tag [%__photo_url__%] with %__tags__% (%__reason__%)
						*/
						// TODO
					}
					if (reply.innerHTML.match("===Start of GroupID===")) { // v1.6.2 and prior
	                    			var groupIdBlock = reply.innerHTML.split("===Start of GroupID===")[1]
                                               			     .split("===End of GroupID===")[0]
                                               			     .replace(/<br>/g, '\n');
						var groupInvites = [];
						if ($chk(groupIdBlock)) {
							groupInvites = groupIdBlock.split(/(?:\n| )/).filter( function (groupId) {
								return groupId.match(/\d+@\w\d{2}/);
							}).map( function (groupId) {
								return { groupId: groupId.match(/(\d+@\w\d{2})/)[1] };
							});
						}
					}
					if (reply.innerHTML.match("===Start of Invites===")) { // v1.6.3 and newer
						/* => per medal
						groupId: 0123456789@N01
						groupId: 1234567890@N34
						reportMissing: http://www.flickr.com/groups/some_backroom/discuss/987654321/
						html: unable to invite [%__photo__url__%] to group 
						<a href="%__invite__group__url__%">%__invite__group__name__%</a> (%__reason__%)
						*/
						// TODO
					}
					if (reply.innerHTML.match("===Start of Referrer===")) {
						var referrer = reply.innerHTML.split("===Start of Referrer===")[1]
									.split("===End of Referrer===")[0]
								   	.replace(/\n/g, '')
									.replace(/^<br>/g, '')
									.replace(/<br>/g, '\n')
									.replace(/\n$/, '');
						if (reply.innerHTML.match("===Start of ReferrerReplacement===")) {
							var referrerReplacement = reply.innerHTML.split("===Start of ReferrerReplacement===")[1]
									.split("===End of ReferrerReplacement===")[0]
									.replace(/\n/g, '')
									.replace(/^<br>/g, '')
									.replace(/<br>/g, '\n')
									.replace(/\n$/, '');
						}
					}
					if (reply.innerHTML.match("===Start of ReportThreads===")) {
					/* => per medal there could be multiple report threads:
					1. post to chat
					2. post to showroom thread
					3. post to back room HOF thread
					===Start of ReportThread===
					groupId: 0123456789@N01
					threadId: 9876543210
					html: %__winner__username__% has won <a href="%__challenge__url__%">%__challenge__title__%</a> with
					<a href="%__winner__photourl__%"><img src="%__winner__image__url__%"/></a>
					===End of ReportThread===
					===Start of ReportThread===
					groupId: ...
					...
					===End of ReportThread===

					or

					===Start of ReportThreads===
					groupId: 0123456789@N01
					threadId: 9876543210
					html: %__winner__username__% has won <a href="%__challenge__url__%">%__challenge__title__%</a> with
					<a href="%__winner__photourl__%"><img src="%__winner__image__url__%"/></a>
					groupId: ...
					...
					===End of ReportThreads===

					but...

					some report threads span all (most) medals:
					. need admin invite
					. need a medal
					...
					*/ // TODO
					    var reportThreads = reply.innerHTML.split("===Start of ReportThreads===")[1]
									.split("===End of ReportThreads===")[0]
									.replace('/<br>/g', '\n');
					    if ($chk(reportThreads)) {
						var reports = [];
						// TODO
					    }
					}
					loadedMedals.medals[loadedMedals.medals.length] = { // don't use array index: there could be a regular comment between medals
						title: title,
						medal: medal,
						tags: tags,
						groupInvites: groupInvites,
						referrer: referrer,
						referrerReplacement : referrerReplacement,
						reports: reports
					};
                		} catch (e) {
                    			GM_log("error reading medal entry - skipping (" + e + ")");
                		}
			});
			GM_log( "Loading medals complete" );
			callback();
			return;
          	    } catch (e) {
            		loadedMedals = {
				success: false,
				error: e
			};
			callback();
			return;
          	    }
               	},
		onFailure: function (response) {
			loadedMedals = {
				success: false,
				error: response.statusText
			};
			callback();
			return;
		}
	}).send(); // end Request
    }

    function loadThemes() {
    	var workflow = groupConfig.workflow();
	if (!$chk(workflow)) {
		return {
			success: false,
			error: 'no workflow for this group'
		};
	}
	var themesThread = workflow.themesThread;
	if (!$chk(themesThread)) {
		return {
			success: false,
			error: 'no themes comment defined in workflow'
		};
	}
	var retval = {
		success: true,
		themes: []
	};
	new Request({
                method:"GET",
                url:themesThread,
		async: false,
                headers:{
                        "User-Agent":"monkeyagent",
                        "Accept":"text/monkey,text/xml"
                },
                onSuccess:function(responseText, responseXML) {
          	    try {
                	var tempDiv = new Element('div', {
                    		html: responseText.stripScripts()
                	});
			if ($chk(tempDiv.getElement('p.Problem'))) {
				retval.success = false;
				retval.error = tempDiv.getElement('p.Problem').innerHTML;
				return;
			}

            		var themesHTML = tempDiv.getElement('div[id=DiscussTopic]').get('html');
			if (!$chk(themesHTML)) {
				retval.success = false;
				retval.error = 'no data found in theme thread';
				return;
			}
			themesHTML.split("===Start of theme list===").each(function (block) {
			    try {
			    	if (block.match("===End of theme list===")) {
					var themelist = block.split( "===End of theme list===" )[ 0 ];
					themelist = themelist.replace(/\n/g, '').split( /<br[^>]*>/ ); //into array
					retval.themes.combine(themelist);
				}
			    } catch (e) {
			    	// ignore
			    }
			});

			GM_log( "Loading themes complete" );
			return;
          	    } catch (e) {
            		retval.success = false;
			retval.error = e;
			return;
          	    }
               	},
		onFailure: function (response) {
			retval.success = false;
			retval.error = response.statusText;
			return;
		}
	}).send(); // end Request
	return retval;
    }

    function loadHeaders() {
    	var workflow = groupConfig.workflow();
	if (!$chk(workflow)) {
		return {
			success: false,
			error: 'no workflow for this group'
		};
	}
	var headersThread = workflow.headersThread;
	if (!$chk(headersThread)) {
		return {
			success: false,
			error: 'no header thread defined in workflow'
		};
	}
	var retval = {
		success: true,
		headers: []
	};
	new Request({
                method:"GET",
                url:headersThread,
		async: false,
                headers:{
                        "User-Agent":"monkeyagent",
                        "Accept":"text/monkey,text/xml"
                },
                onSuccess:function(responseText, responseXML) {
          	    try {
                	var tempDiv = new Element('div', {
                    		html: responseText.stripScripts()
                	});

			if ($chk(tempDiv.getElement('p.Problem'))) {
				retval.success = false;
				retval.error = tempDiv.getElement('p.Problem').innerHTML;
				return;
			}
			try {
            			var replies = tempDiv.getElement('div[id=DiscussTopic]').getElement('table.TopicReply').getElements('td.Said');
			} catch (e) {
				retval.success = false;
				retval.error = 'no replies found in medal thread';
				return;
			}
			if (replies.snapshotLength == 0) {
				retval.success = false;
				retval.error = 'no replies found in header thread';
				return;
			}
            		replies.each(function (reply) {
                		try {
		                    var title = reply.innerHTML.split("===Start of Title===")[1]
                                               .split("===End of Title===")[0]
					       .replace(/<br>/g, '\n')
                                               .replace(/<[^>]+>/g, '');
		                    var header = reply.innerHTML.split("===Start of Header===")[1]
                                                .split("===End of Header===")[0]
						.replace(/\n/g, '')
						.replace(/^<br>/, '');
		                    if (reply.innerHTML.match("===Start of Theme===")) {
                		        var theme = reply.innerHTML.split("===Start of Theme===")[1]
                                                   .split("===End of Theme===")[0]
						   .replace(/\n/g, '')
						   .replace(/^\s*(.*)\s*$/, '$1')
                                                   .replace(/<[^>]+>/g, '');
				    }
				    var titles = undefined; //title.split(/\n/g);
				    if (!$chk(titles)) {
				    	//GM_log("single line title");
				    	titles = [];
					titles[0] = title;
				    }
				    var themes = undefined; // $chk(theme) ? theme.split(/\n/g) : [];
				    if (!$chk(themes)) {
				    	//GM_log("single line theme");
				    	themes = [];
					themes[0] = theme;
				    }
				    if (themes.length <= 1 && titles.length == 1) { // the most common case
				    	retval.headers[retval.headers.length] = {	// don't use array index: there could be a regular comment between medals
						title: titles[0],
						header: header,
						theme: themes.length > 0 ? themes[0] : undefined
					};
				    } else {
				    // TODO
				    GM_log("multiple titles, or multiple themes per header not supported yet");
				    	// it makes sense to have multiple titles to share the same header, and free theme:
					// #1 - #10: Regular challenge
					// could be defined as 
					// #1: Regular challenge
					// #2: Regular challenge
					// ...
					// all with the same header, and no fixed theme
					//
					// it makes sense to have multiple titles to share the same header, with one fixed theme:
					// #1: Regular challenge
					// #2: Regular challenge
					// ...
					// theme: 
					// OPEN - 
					//
					// it does not make sense to have 1 title with multiple fixed themes: the script would not know which one to choose
					// possible solution: replace the theme list, with a 'fixed theme list'
					//
					// it makes sense to have n titles share the same header, with n fixed themes; 1-1 relationship
					// #13: Diamond Heart Challenge
					// #14: Diamond Heart Challenge
					// themes:
					// 13 - OPEN - Diamond Heart Challenge - Anything Goes!
					// 14 - OPEN - Diamond Heart Challenge 
				    }
                		} catch (e) {
                    			GM_log("error reading header entry - skipping (" + e + ")");
					//GM_log("html: " + reply.innerHTML);
                		}
			});
			GM_log( "Loading headers complete" );
			return;
          	    } catch (e) {
            		retval.success = false;
			retval.error = e;
			return;
          	    }
               	},
		onFailure: function (response) {
			retval.success = false;
			retval.error = response.statusText;
			return;
		}
	}).send(); // end Request
	return retval;
    }

var remoteActions = [];
var failedRemoteActions = [];
var expectedRemoteActions = new Hash();
var winnersHashes = [];

var SIMULATE = false;

    function listPlayers(data) {
	var playersElement = data.playersElement;
	var playerHash = data.playerHash;
	var ucpThread = data.ucpThread;
	var idPrefix = data.idPrefix;
	var radio = data.radio;
	var winnerChangeCallback = data.winnerChangeCallback;

	try {
		if (idPrefix.match('winner')) {
			var votesInLastvote = ucpThread.votes()[ucpThread.votes().length - 1].options.votesArray.length - 1;
			if (votesInLastvote > ucpThread.photos().length) {
				new Element('label', {
					html: 'there are votes for ' + votesInLastvote + ' entries, but the script found ' + ucpThread.photos().length + ' photos - automatic winner detection could be wrong',
					styles: {
						color: 'red'
					}
				}).inject(new Element('td', { colspan: 3 }).inject(new Element('tr').inject($('UCPANG-workflow-wizard-current-title-row'), 'before')));
			}
		}
	} catch (e) {
		GM_log("error checking number of entries: " + e);
	}

	ucpThread.photos().each( function (competitor, idx) {
		playersElement.adopt(
		     new Element('div').adopt(
			new Element('input', {
				type: radio == true ? 'radio' : 'checkbox',
				name: idPrefix,
				id: idPrefix + idx,
				value: idx,
				events: {
					change: function (evt) {
						var button = $(evt.target);
						if (radio == true) {
							var idx = button.value
							playerHash.empty();
							playerHash.set(idx, ucpThread.photos()[idx]);
						} else {
							var idx = parseInt(button.id.replace(idPrefix, ''), 10);
							if (button.checked) {
								playerHash.set(idx, ucpThread.photos()[idx]);
							} else {
								playerHash.erase(idx);
							}
						}
						if (winnerChangeCallback) {
							winnerChangeCallback(playerHash);
						}
					}
				}
			}),
			new Element('label', {
				html: competitor.poster().username + " (" + ($chk(competitor.photoTitle()) ? competitor.photoTitle() : "<i>--untitled--</i>") + ")",
				'for': idPrefix + idx
			})
		    )
		);
	    });
    }

    function listMedals(data) {
    	var idPrefix = data.idPrefix;
	var loadedMedals = data.loadedMedals;
	var medalElement = data.medalElement;
	var medalPreviewElement = data.medalPreviewElement;

		    if (loadedMedals.success == false) {
			new Element('p', { html: '<i>' + loadedMedals.error + '</i>' }).inject(medalElement);
		    } else if (loadedMedals.medals.length == 0) {
		    	new Element('p', { html: '<i>no medals are defined for this group - contact Alesa Dam for support</i>' }).inject(medalElement);
		    } else {
		    	var medalCombo = new Element('select', {
				id: idPrefix + 'medal-combo',
				events: {
					'change': function (evt) {
						var combo = $(evt.target);
						var selected = combo.getSelected().get('value');
						var tags = '--none--';
						var groups = '--none--';
						var medalText = '--none--';
						if (selected > -1) {
						    try {
							var medal = loadedMedals.medals[selected];
							var referrer = medal.referrer;
							if ($chk(referrer)) {
							    if (referrer.match('%__referrer')) {
								referrer = referrer.replace(/%__referrer__url__%/g, document.location.href);
		    						var currentTitle = $('GoodStuff').getElement('h2').get('html');
								var themeExtract  = groupConfig.workflow().themeExtractRegExp;
								if ($chk(themeExtract)) {
									var theme = currentTitle.replace(new RegExp(themeExtract.expression, themeExtract.flags), themeExtract.replacement);
									referrer = referrer.replace(/%__referrer__theme__%/g, theme);
								}
								var numberExtract = groupConfig.workflow().numberExtractRegExp;
								if ($chk(numberExtract)) {
									var number = currentTitle.replace(new RegExp(numberExtract.expression, numberExtract.flags), numberExtract.replacement);
									referrer = referrer.replace(/%__referrer__number__%/g, number);
								}
							    }
							    var referrerReplacement = medal.referrerReplacement;
							    if ($chk(referrerReplacement)) {
								medalText = medal.medal.replace(referrerReplacement, referrer);
							    } else {
								medalText = medal.medal + ($chk(referrer) ? referrer : '');
							    }
							} else {
							    medalText = medal.medal;
							}
                                                        if (!$chk(medalText) || medalText.replace(/\n/g, '').length == 0) {
                                                                medalText = '--none--';
                                                        } else {
                                                                medalText = medalText.replace(/\n/g, '<br>');
                                                        }
							tags = medal.tags;
							if (!$chk(tags) || tags.replace(/\n/g, '').length == 0) {
								tags = '--none--';
							}
							groups = medal.groupInvites;
							if (!$chk(groups) || groups.length == 0) {
								groups = '--none--';
							}
						    } catch (e) {
						    	alert(e);
						    }
						}
						if ($$('select[id*=UCPANG-workflow]').every(function (comboList) {
								if (comboList.get('id').match('medal-combo')) {
									return (comboList.getSelected().get('value') > -1);
								} else {
									// ignore other combos
									return true;
								}
							})) {
							$('UCPANG-promote-wizard-save-button').set('style', 'background-color:;');
							$('UCPANG-promote-wizard-save-button').set('html', 'SAVE');
						} else {
							$('UCPANG-promote-wizard-save-button').set('style', 'background-color: orange;');
							$('UCPANG-promote-wizard-save-button').set('html', 'SAVE without a medal?');
						}
						try {
						$(idPrefix + 'tags').set('html', tags.replace(/\n/g,'<br>'));
						$(idPrefix + 'invites').empty();
						if (groups == '--none--') {
							$(idPrefix + 'invites').set('html', groups);
						} else {
							groups.forEach( function (group, idx) {
								var groupId = group.groupId;
								var groupDiv = new Element('div', {
									id: 'UCPANG-workflow-invitegroup-' + groupId
								}).inject($(idPrefix + 'invites'));
								var groupAdminData = isGroupAdministratorOrModerator(userNsid, groupId);
								new Element('input', {
									type: 'checkbox',
									id: 'UCPANG-workflow-invitegroup-checkbox-' + idx,
									checked: groupAdminData.administrator || $chk(loadedMedals.inviteThread),
									title: groupAdminData.administrator ? 'invite into group pool' : 
										$chk(loadedMedals.inviteThread) ? 'post in invite thread' :
										'you are not an administrator for this group'
								}).inject(groupDiv);
								new Element('label', {
									'for': 'UCPANG-workflow-invitegroup-checkbox-' + idx,
									html: groupAdminData.groupname,
									title: groupAdminData.administrator ? 'invite into group pool' : 
										$chk(loadedMedals.inviteThread) ? 'post in invite thread' :
										'you are not an administrator for this group',
									styles: {
										color: (groupAdminData.administrator || $chk(loadedMedals.inviteThread)) ? '' : 'grey'
									}
								}).inject(groupDiv);
							});
						}
						$(idPrefix + 'medaltext').set('html', medalText);
						} catch (e) {
							alert(e);
						}
					}
				}
			}).inject(medalElement);
	    		new Element('option', { html: '--- select a medal ---', value: -1 }).inject(medalCombo);
			loadedMedals.medals.each(function (medal, idx) {
				new Element('option', { html: medal.title, value: idx }).inject(medalCombo);
			});
			// color the SAVE button
			if ($chk($('UCPANG-promote-wizard-save-button'))) {
				$('UCPANG-promote-wizard-save-button').set('style', 'background-color: orange;');
				$('UCPANG-promote-wizard-save-button').set('html', 'SAVE without a medal?');
			}
		    }
                    createEditableTextElement(medalPreviewElement, idPrefix + 'medaltext', 'white', '--none--');

		    var extrasTable = new Element('table').inject(medalPreviewElement);
		    var tagsRow = new Element('tr', { 'class': idPrefix + 'list-players-row' } ).inject(extrasTable);
		    new Element('td').inject(tagsRow);
		    new Element('td', { html: 'add&nbsp;tag(s):',
					id: idPrefix + 'tags-label' }).inject(tagsRow);
		    var tagsCell = new Element('td').inject(tagsRow);
		    createEditableLabelElement(tagsCell, idPrefix + 'tags', '--none--');

		    var invitesRow = new Element('tr', { 'class': idPrefix + 'list-players-row', valign: 'top' }).inject(tagsRow, 'after');
		    new Element('td').inject(invitesRow);
		    new Element('td', { html: 'invites:',
				id: idPrefix + 'invites-label' }).inject(invitesRow);
		    var invitesCell = new Element('td', {
		    	id: idPrefix + 'invites', 
			html: '--none--'
		    }).inject(invitesRow);

    }

    function toggleVoteToClosedWizardDialog(ucpThread) {
	if (SIMULATE) alert("simulating");
	if ($chk($('UCPANG-promote-wizard-dialog'))) {
		$('UCPANG-promote-wizard-dialog').destroy();
	} else {
            wizardDialog = new Element('table', {
		border: 0,
		id: 'UCPANG-promote-wizard-dialog',
                styles: ucpDialogStyle
	    });
	    var closeThreadRow = new Element('tr').inject(wizardDialog);
	    new Element('td', { html: 'close thread:',
				id: 'UCPANG-workflow-wizard-close-thread-label' }).inject(closeThreadRow);
	    var closeThreadCell = new Element('td').inject(closeThreadRow);
	    new Element('input', { type: 'checkbox', disabled: true, checked: true }).inject(closeThreadCell);

	    var closingPost = groupConfig.workflow().closingPost;
	    if ($chk(closingPost)) {
	    	var closingPostRow = new Element('tr').inject(wizardDialog);
		new Element('td', { html: 'add final post:' }).inject(closingPostRow);
    		createEditableLabelElement(new Element('td').inject(closingPostRow), 'UCPANG-workflow-wizard-closingpost', closingPost.text);
	    }

	    var autoRow = new Element('tr').inject(wizardDialog);
	    var autoCell = new Element('td', {
	    	colspan: 3
	    }).inject(autoRow);
	    new Element('input', {
	    	type: 'checkbox',
		checked: groupPreferences.workflowAutoClaimOwnership(),
		id: 'UCPANG-workflow-wizard-auto-checkbox',
		events: {
			change: function (evt) {
				groupPreferences.setWorkflowAutoClaimOwnership(evt.target.checked);
			}
		}
	    }).inject(autoCell);
	    new Element('label', {
	    	html: 'close automatically on opening this dialog',
		'for': 'UCPANG-workflow-wizard-auto-checkbox'
	    }).inject(autoCell);
	    var buttonRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    var buttonCell = new Element('td', {
		colspan: 3,
		align: 'right',
		valign: 'top'
	    }).inject(buttonRow);
	    new Element('button', {
		'class': 'CancelButt',
		html: 'Cancel',
		events: {
			'click': function (evt) {
				$('UCPANG-promote-wizard-dialog').destroy();
			}
		}
	    }).inject(buttonCell);
	    new Element('span', {
		html: ' '
	    }).inject(buttonCell);
	    var saveButton = new Element('button', {
		'class': 'Butt',
		html: 'CLOSE',
		id: 'UCPANG-promote-wizard-save-button',
		events: {
			'click': function (evt) {
				var closeButton = $(evt.target);
				if (!closeButton.hasClass('Butt')) return;
				closeButton.set('class', 'DisabledButt');
				closeButton.empty();
				var allIsRight = true;
				new Element('img', { src: updatingIcon }).inject(closeButton);
				if ($chk($('UCPANG-workflow-wizard-closingpost'))) {
					closeButton.set('title', 'posting closing text');
					var closingPost = $('UCPANG-workflow-wizard-closingpost').get('html');
					new Request({
						async: false,
						url: document.location.href + '/#reply',
						data: {
							done: 1,
							message: closingPost
						},
						onFailure: function (response) {
							closeButton.getElement('img').set('src', errorIcon);
							closeButton.set('title', 'error: ' + response.statusText);
							allIsRight = false;
						},
						onSuccess: function (responseText, responseXml) {
							// nothing to do
						}
					}).post();
				}
				if (!allIsRight) {
					return;
				}
				closeButton.set('title', 'retrieving thread');
				new Request({
					async: false,
					url: document.location.href,
					onFailure: function(response) {
						closeButton.getElement('img').set('src', errorIcon);
						closeButton.set('title', 'error: ' + response.statusText);
					},
					onSuccess: function (responseText, responseXML) {
                				var tempDiv = new Element('div');
					        tempDiv.innerHTML = responseText.stripScripts();
					        var problem = tempDiv.getElement('p.Problem');
					        if ($chk(problem)) {
							closeButton.getElement('img').set('src', errorIcon);
							closeButton.set('title', 'error: ' + problem.innerHTML);
					        } else {
							if (ucpThread.isClosed(tempDiv).closed) {
								closeButton.getElement('img').set('src', errorIcon);
								closeButton.set('title', 'already closed');
								ucpThread.isClosed(tempDiv).closer.inject(new Element('div').inject(buttonRow, 'before'));
								new Element('div', { html: 'WARNING -- already claimed<br>this person is probably cleaning up this challenge', align: 'center' }).inject(buttonRow, 'before');
							} else { // close it!
								closeChallengeThread(function (retval) {
									if (!retval.success) {
										closeButton.getElement('img').set('src', errorIcon);
										closeButton.set('title', retval.msg);
									} else {
										closeButton.getElement('img').set('src', defaultCheckIconSmall);
										closeButton.set('title', 'closed - owner now');
										document.location.href = /(.*flickr.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(document.location.href)[1] + '/';
									}
								});
							}
						}
					}
				}).get();
			}
		}
	    }).inject(buttonCell);

	    wizardDialog.inject($('UCPANG-workflow-cell'), 'after');
	    if (groupPreferences.workflowAutoClaimOwnership()) {
	    	saveButton.click();
	    }
	}
    }

    function toggleClosedToCleaningWizardDialog(ucpThread) {
	if (SIMULATE) alert("simulating");
	if ($chk($('UCPANG-promote-wizard-dialog'))) {
		$('UCPANG-promote-wizard-dialog').destroy();
	} else {
	    remoteActions = [];
	    failedRemoteActions = [];

            wizardDialog = new Element('table', {
		border: 0,
		id: 'UCPANG-promote-wizard-dialog',
                styles: ucpDialogStyle
	    }).inject($('UCPANG-workflow-cell'), 'after');

	    var lastvote = undefined;
	    var score = ucpThread.cummulativeScore();
	    if (ucpThread.votes().length == 0) {
	    	new Element('td', {
			html: 'could not determine winner: no votes found',
			colspan: 3,
			styles: {
				color: 'red'
			}
		}).inject(new Element('tr').inject(wizardDialog));
		var maxVote = 0;
	    } else {
	        lastvote = ucpThread.votes()[ucpThread.votes().length - 1];
		var lastVoteCell = new Element('td', {
			colspan: 3,
			html: 'decision based on score: <b>' + ucpThread.scoreSummary(false) + '</b> (last vote by ' + lastvote.poster().username + ')'
		}).inject(new Element('tr').inject(wizardDialog));
		maxVote = score.maxVote;
		if (maxVote < ucpThread.challengeDefinition().neededScore()) {
		    new Element('span', {
			html: '  maximum score is ' + maxVote + ', but required score is ' + ucpThread.challengeDefinition().neededScore(),
			styles: {
				color: 'red'
			}
		    }).inject(lastVoteCell);
		}
	    }
	    // when cleaning, we need to change the title *back* to OPEN!
	    var titles = provideTitleChangeRow({
	    	table: wizardDialog,
	    	titleReplacer: groupConfig.workflow().fromVoteToOpenRegExp,
	    	nextStepChecker: groupConfig.workflow().fromOpenToVoteRegExp
	    });
	    var newTitle = titles.newTitle;
	    // preview the new defender
	    var playersRow = new Element('tr', { valign: 'top' } ).inject(wizardDialog);
	    new Element('td', { html: 'winner: ' }).inject(playersRow);
	    playersElement = new Element('td', { id: 'UCPANG-workflow-winner-list-players-cell' } ).inject(playersRow);
	    if (winnersHashes['winner'] == undefined) {
		winnersHashes['winner'] = new Hash();
	    }
	    listPlayers( {
		playersElement: $('UCPANG-workflow-winner-list-players-cell'),
		playerHash: winnersHashes['winner'],
		ucpThread: ucpThread,
		idPrefix: 'UCPANG-workflow-winner-',
		radio: true,
		winnerChangeCallback: function (playerHash) {
			var winningPhoto;
			playerHash.forEach(function (photo, key) { // used a radio button: there is only one
				winningPhoto = photo;
			});
			var winner, loser;
			ucpThread.photos().each( function (photo) {
				if (photo.photoId() == winningPhoto.photoId()) {
				    if (winner == undefined) {
					winner = photo;
				    }
				} else {
				    if (loser == undefined) {
					loser = photo;
				    }
				}
			});
			var winnerNode = winner.node();
			var loserNode  = loser.node();

			var winnerClone = winnerNode.clone(true);
			var winnerHtml = stripChallengeAnnouncement(winnerClone);
			winnerClone = new Element('div', { html: winnerHtml });
			winnerClone.getElements('img[alt*=UCPANG], img[class*=FPAOTP]').dispose();
			// add small version of the losing photo
			// if it's the first thumbnail added, insert a break
			// thumbnails, and images with a width of less than 275 are not considered photos in ucpCheckPhoto
			// => check manually
			if (winnerClone.getElements('img[src*=.jpg]').length == 1) {
				new Element('br').inject(winnerClone);
				new Element('br').inject(winnerClone);
			}
			var loserSrc = loserNode.getElements('img').filter(ucpCheckPhoto,ucpThread.challengeDefinition())[0].get('src').replace(/(^.*flickr.com\/.*\/\d+_[^_]+)(?:_\w)?\.jpg$/, "$1_t.jpg");
			var loserUrl = loserNode.getElement('a').get('href');
			var thumbDimensions = getThumbDimensions(loser.photoId());
			var loserThumb = new Element('img', { src: loserSrc, width: thumbDimensions.width > thumbDimensions.height ? 90 : 75 }).inject(new Element('a', { href: loserUrl }).inject(winnerClone));
			// print the score
			new Element('b', { html: lastvote.voteText()[0].replace(/[^\d]*(\d+)[^\d](\d+)[^\d]*/, "$1:$2") }).inject(winnerClone);
			// update the preview
			$('UCPANG-workflow-newdefender-who').set('html', '<b><u>' + winner.poster().username + '</u> says:</b>');
			$('UCPANG-workflow-newdefender-said').set('html', winnerClone.get('html'));
			// correct permalink
	    		var normalizedThreadURL = /(.*flickr\.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(location.href)[0];
			var permalink = normalizedThreadURL + '/' + winnerNode.getParent('tr').getElement('td.Who a').get('name').replace('comment', '');
			$('UCPANG-workflow-newdefender-permalink').empty();
			new Element('a', { html: 'permalink', href: permalink }).inject($('UCPANG-workflow-newdefender-permalink'));
			// update the losing image fields
			var loserClone = loserNode.clone(true);
			loserClone.getElements('small').dispose();
			loserClone.getElement('img').set('width', 250);
			$('UCPANG-workflow-losing-image').set('html', loserClone.get('html'));
			permalink = normalizedThreadURL + '/' + loserNode.getParent('tr').getElement('td.Who a').get('name').replace('comment', '');
			$('UCPANG-workflow-losing-permalink').empty();
			new Element('a', { html: 'permalink', href: permalink }).inject($('UCPANG-workflow-losing-permalink'));
		}
	    });
	    // show new defender preview
	    var newDefenderRow = new Element('tr', { valign: 'top' } ).inject(wizardDialog);
	    new Element('td', { html: 'defender&nbsp;preview:&nbsp;' }).inject(newDefenderRow);
	    new Element('td', { 'class': 'Said', id: 'UCPANG-workflow-newdefender-who' }).inject(newDefenderRow);
	    new Element('td', { id: 'UCPANG-workflow-newdefender-permalink' }).inject(newDefenderRow);
	    newDefenderRow = new Element('tr', { valign: 'top' } ).inject(wizardDialog);
	    new Element('td').inject(newDefenderRow);
	    var newDefenderPreview = new Element('td', { width: 500 }).inject(newDefenderRow);
            createEditableTextElement(newDefenderPreview, 'UCPANG-workflow-newdefender-said', 'white', '--none--');

	    // list replies to be removed:
	    new Element('td', { colspan: 3, html: 'remove replies: '}).inject(new Element('tr').inject(wizardDialog));
	    // the losing image:
	    var losingImageRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    new Element('td').inject(losingImageRow);
	    var losingImageCell = new Element('td').inject(losingImageRow);
	    new Element('input', { type: 'checkbox', checked: false, id: 'UCPANG-delete-losing-image' }).inject(losingImageCell);
	    new Element('label', { html: '&nbsp;losing&nbsp;contestant: ', 'for': 'UCPANG-delete-losing-image'}).inject(losingImageCell);
	    var losingPermalinkCell = new Element('td', { id: 'UCPANG-workflow-losing-permalink' }).inject(losingImageRow);

	    losingImageRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    new Element('td').inject(losingImageRow);
	    losingImageCell = new Element('td').inject(losingImageRow);
	    new Element('div', { styles: { 'background-color': 'white', border: '1px solid' }, id: 'UCPANG-workflow-losing-image' } ).inject(losingImageCell);

	    // simulate a mouse click on the winners
	    ucpThread.photos().each( function (competitor, idx) {
	    	if (score.options.votesArray[idx+1] == maxVote) {
	    		$('UCPANG-workflow-winner-' + idx).click();
		}
	    });
	    var normalizedThreadURL = /(.*flickr\.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(location.href)[0];
	    [ ucpThread.votes(), ucpThread.comments() ].each( function (replyArray, idx) {
	    	if (idx == 0) {
	    		// the votes:
	    		wizardDialog.adopt(new Element('tr').adopt(
	    			new Element('td'),
				new Element('td', { html: '<b>votes:</b>' })
	    		));
		} else {
	    		// other replies:
	    		wizardDialog.adopt(new Element('tr').adopt(
	    			new Element('td'),
				new Element('td', { html: '<b>comments:</b>' })
	    		));
		}
	    	replyArray.each( function (reply, replyidx) {
	    	    // TODO: allow the last vote to be deleted too
			var enabled = idx > 0 || replyidx < replyArray.length - 1;
	    		var replyCell = reply.node();
			var permalink = normalizedThreadURL + '/' + replyCell.getParent('tr').getElement('td.Who a').get('name').replace('comment', '');
			// remove unnecessary info
			var clone = replyCell.clone();
			clone.getElements('h4').destroy();
			clone.getElements('small').destroy();
			var removeReplyRow = new Element('tr').inject(wizardDialog);
			new Element('td').inject(removeReplyRow); // empty first column
			var delCell = new Element('td', { nowrap: 'nowrap' }).inject(removeReplyRow);
			new Element('input', { type: 'checkbox', checked: enabled, id: 'UCPANG-delete-reply-' + idx + '-' + replyidx, 'class': 'UCPANG-delete-reply-check', value: permalink }).inject(delCell);
			new Element('label', { html: '&nbsp;<u><i>' + reply.poster().username + "</i></u>: ", 'for': 'UCPANG-delete-reply-' + idx + '-' + replyidx }).inject(delCell);
			new Element('span', { html: clone.get('html'), styles: { display: 'inline-block' } }).inject(delCell);
			new Element('span', { html: '&nbsp;' }).inject(delCell);
			new Element('a', { href: permalink, html: 'permalink' }).inject(new Element('td').inject(removeReplyRow));
	    	});
	    });
	    // 'xxx has voted last'
	    var closingRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    new Element('td', { html: '<b>add reply:</b>' }).inject(closingRow);
	    var closingReplyCell = new Element('td').inject(closingRow);
	    createEditableTextElement(closingReplyCell, 'UCPANG-workflow-wizard-closing-reply', 'white', '<b>' + lastvote.poster().username + '</b> voted last');

	    // button row
	    remoteActions = [];
	    failedRemoteActions = [];
	    var buttonRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    var buttonCell = new Element('td', {
		colspan: 3,
		align: 'right',
		valign: 'top'
	    }).inject(buttonRow);
	    new Element('button', {
		'class': 'CancelButt',
		html: 'Cancel',
		events: {
			'click': function (evt) {
				$('UCPANG-promote-wizard-dialog').destroy();
			}
		}
	    }).inject(buttonCell);
	    new Element('span', {
		html: ' '
	    }).inject(buttonCell);
	    new Element('button', {
		'class': 'Butt',
		html: 'SAVE',
		id: 'UCPANG-promote-wizard-save-button',
		events: {
			'click': function (evt) {
			    try {
				// show spinning balls
				var button = $(evt.target);
				if (button.hasClass('DisabledButt')) {
					return;
				}
				button.set('class', 'DisabledButt');
				button.empty();
				button.adopt(new Element('img', {
					src: updatingIcon,
					title: 'updating challenge'
				}));
				expectedRemoteActions.set('closingreply', true);
				expectedRemoteActions.set('update-title', true);

				var wizardDialog = $('UCPANG-promote-wizard-dialog');
				// update title
				remoteActions.push(true);
				expectedRemoteActions.erase('update-title');
				new Element('img', { src: updatingIcon, title: 'updating title' }).inject($('UCPANG-workflow-wizard-new-title-label'));
				updateChallengeTitle($('UCPANG-workflow-wizard-newtitle').get('text'), function (result) { // use 'text', not 'html'
					var button = $('UCPANG-workflow-wizard-new-title-label').getElement('img');
					updateRemoteAction(result.success, 'error updating title' + result.msg, button, 'title updated');
				});
				// add closing reply
				// if successful: update new defender
				// if successful: delete replies and losing image
        			var url = /.*flickr.com\/groups\/[^\/]+\/discuss\/\d+/.exec(ucpThread.url());

				remoteActions.push(true);
				expectedRemoteActions.erase('closingreply');
				var closingReplyPosted = false;
				new Element('img', {
					src: updatingIcon,
					title: 'posting'
				}).inject($('UCPANG-workflow-wizard-closing-reply').getParent('tr').getElement('td'));
				postThreadReply({
					async: false,
					threadUrl: url,
					message: $('UCPANG-workflow-wizard-closing-reply').get('html').replace(/\n/g, '').replace(/<br[^>]*>/g, '\n'),
					callback: function (result) {
						closingReplyPosted = result.success;
						if (closingReplyPosted) {
							expectedRemoteActions.set('update-defender', true);
						}
						updateRemoteAction(result.success, 
							'error posting closing reply: ' + result.msg, 
							$('UCPANG-workflow-wizard-closing-reply').getParent('tr').getElement('td img'), 
							'reply posted');
					}
				});
				if (!closingReplyPosted) {
					return;
				}

				var newDefenderText = $('UCPANG-workflow-newdefender-said').get('html').replace(/\n/g, '').replace(/<br[^>]*>/g, '\n');
				var newDefenderPermalink = $('UCPANG-workflow-newdefender-permalink').getElement('a').get('href');
				remoteActions.push(true);
				expectedRemoteActions.erase('update-defender');
				new Element('img', { 
					src: updatingIcon, 
					title: 'updating defender' 
				}).inject($('UCPANG-workflow-newdefender-said').getParent('tr').getElement('td'));
				var updatedDefender = false;
				updateThreadReply({
					async: false,
					replyUrl: newDefenderPermalink,
					message: newDefenderText,
					callback: function (result) {
						var button = $('UCPANG-workflow-newdefender-said').getParent('tr').getElement('td img');
						updatedDefender = result.success;
						if (updatedDefender) {
							expectedRemoteActions.set('delete-votes-and-comments', 'true');
							expectedRemoteActions.set('delete-losing-contestant', 'true');
						}
						updateRemoteAction(result.success, 'error posting update: ' + result.msg, button, 'update posted');
					}
				});
				if (!updatedDefender) {
					return;
				}

				var deleteLoserCheckbox = $('UCPANG-delete-losing-image');
				if (!deleteLoserCheckbox.checked) {
					expectedRemoteActions.erase('delete-losing-contestant');
				}

				$$('input.UCPANG-delete-reply-check').each(function (checkbox) {
					if (checkbox.checked) {
						expectedRemoteActions.set(checkbox.value, 'true');
					}
				});
				expectedRemoteActions.erase('delete-votes-and-comments');
				$$('input.UCPANG-delete-reply-check').each(function (checkbox) {
					if (!checkbox.checked) {
						return;
					}
					remoteActions.push(true);
					expectedRemoteActions.erase(checkbox.value);
					new Element('img', { 
						src: updatingIcon, 
						title: 'deleting reply'
					}).inject(checkbox, 'before');
					var replyUrl = checkbox.value;
			    		deleteThreadReply( {
			    			replyPermalink: replyUrl,
						referrer: ucpThread.url(),
						async: true,
						callback: function (result) {
							updateRemoteAction(result.success, 
								'error deleting reply: ' + result.msg, 
								checkbox.getParent('td').getElement('img'), 
								'reply deleted');
						}
			    		});
				});

				if (deleteLoserCheckbox.checked) {
					remoteActions.push(true);
					expectedRemoteActions.erase('delete-losing-contestant');
					new Element('img', {
						src: updatingIcon,
						title: 'removing losing contestant'
					}).inject(deleteLoserCheckbox, 'before');
					deleteThreadReply( {
						replyPermalink: $('UCPANG-workflow-losing-permalink').getElement('a').get('href'),
						referrer: ucpThread.url(),
						async: true,
						callback: function (result) {
							updateRemoteAction(result.success,
								'error removing losing contestant: ' + result.msg,
								deleteLoserCheckbox.getParent().getElement('img'),
								'contestant deleted');
						}
					});
				}
			    } catch (e) {
			    	GM_log("error cleaning thread: " + e);
			    }
			}
		}
	    }).inject(buttonCell);
	}
    }

function createProgressElement(label, id, action) {
	var progressPanel = $('UCPANG-workflow-wizard-progress-panel');
	var progressElement = new Element('div', {
		id: id,
		events: {
			'run-action': function (evt) {
				$(id + '-icon').set('src', updatingIcon);
				$(id + '-icon').set('title', 'running..');
				action()
			}
		}
	}).inject(progressPanel);
	new Element('img', { src: images.queued, id: id + '-icon', title: 'queued' }).inject(progressElement);
	new Element('span', { html: ' ' + label + ' ' }).inject(progressElement);
	return progressElement;
}

    function toggleClosedToAwardedWizardDialog(ucpThread) {
	if (SIMULATE) alert("simulating");
	if ($chk($('UCPANG-promote-wizard-dialog'))) {
		$('UCPANG-promote-wizard-dialog').destroy();
	} else {
	    if (loadedMedals == undefined) {
	    	loadMedals(function() { toggleClosedToAwardedWizardDialog(ucpThread); });
		return;
	    }
	    remoteActions = [];
	    failedRemoteActions = [];

            wizardDialog = new Element('table', {
		border: 0,
		id: 'UCPANG-promote-wizard-dialog',
                styles: ucpDialogStyle
	    }).inject($('UCPANG-workflow-cell'), 'after');

	    var lastvote = undefined;
	    var score = ucpThread.cummulativeScore();
	    if (ucpThread.votes().length == 0) {
		new Element('td', {
			html: 'could not determine winner: no votes found',
			colspan: 3,
			styles: {
				color: 'red'
			}
		}).inject(new Element('tr').inject(wizardDialog));
		var maxVote = 0;
	    } else {
		lastvote = ucpThread.votes()[ucpThread.votes().length - 1];
		var lastVoteCell = new Element('td', {
			colspan: 3,
			html: 'decision based on score: <b>' + ucpThread.scoreSummary(false) + '</b> (last vote by ' + lastvote.poster().username + ')'
		}).inject(new Element('tr').inject(wizardDialog));
	    	maxVote = score.maxVote;
		if (maxVote < ucpThread.challengeDefinition().neededScore()) {
		    new Element('span', {
			html: '  maximum score is ' + maxVote + ', but required score is ' + ucpThread.challengeDefinition().neededScore(),
			styles: {
				color: 'red'
			}
		    }).inject(lastVoteCell);
		}
	    }

	    var fromVoteToClosed = groupConfig.workflow().fromVoteToClosedRegExp;
	    var sweep = ucpThread.isUnanimous() && ucpThread.challengeDefinition().scoresAdded();
	    if (sweep) {
	    	new Element('td', {
			html: 'found a sweep!',
			styles: {
				color: 'red'
			}
		}).inject(new Element('tr').inject(wizardDialog));
		if ($chk(fromVoteToClosed.sweepreplacement)) {
			fromVoteToClosed.replacement = fromVoteToClosed.sweepreplacement;
		}
		if ($chk(fromVoteToClosed.sweepreplacement2)) {
			fromVoteToClosed.replacement2 = fromVoteToClosed.sweepreplacement2;
		}
	    }
	    var titles = provideTitleChangeRow({
	    	table: wizardDialog,
	    	titleReplacer: fromVoteToClosed,
	    	nextStepChecker: groupConfig.workflow().fromClosedToNewRegExp,
	    });
	    var newTitle = titles.newTitle;

	    [ 'winner', 'second', 'third', 'fourth', 'fifth' ].each(function (place) {
		var nthRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
		var nthSelectCell = new Element('td', { colspan: 2 }).inject(nthRow);
		new Element('input', { 
		    type: 'checkbox', 
		    id: 'UCPANG-workflow-' + place + '-awarding',
		    events: {
			'change': function (evt) {
				var checkbox = $(evt.target);
				var place = checkbox.get('id').replace(/UCPANG-workflow-([^-]+)-awarding/, '$1');
				if (loadedMedals == undefined) {
					var winnersLabel = $('UCPANG-workflow-' + place + '-awarding');
					var loadingMedalsImg = new Element('img', { src: updatingIcon, title: 'loading medals..' }).inject(winnersLabel);
					var loadingMedalsLabel = new Element('i', { html: ' loading medals..' }).inject(winnersLabel);
					loadingMedalsImg.dispose();
					loadingMedalsLabel.dispose();
				}
				if (!checkbox.checked) { // un-checked
					$('UCPANG-workflow-' + place + '-list-players-cell').empty();
					$('UCPANG-workflow-' + place + '-medaltext-cell').dispose();
				} else {
					if (winnersHashes[place] == undefined) {
						winnersHashes[place] = new Hash();
					}
					listPlayers( {
						playersElement: $('UCPANG-workflow-' + place + '-list-players-cell'),
						playerHash: winnersHashes[place],
						ucpThread: ucpThread,
						idPrefix: 'UCPANG-workflow-' + place + '-',
						winnerChangeCallback: place == 'winner' ? function (playerHash) {
							var winnerName = "";
							playerHash.forEach(function (value, key) {
								if (winnerName.length > 0) winnerName += ' - and - ';
								winnerName += value//.poster().username
							});
							$('UCPANG-workflow-wizard-newtitle').set('html', newTitle.replace('%__username__%', winnerName));
						} : undefined
					});
					var medalTextRow = new Element('tr', { valign: 'top' }).inject(checkbox.getParent('tr'), 'after');
                			new Element('td', { id: 'UCPANG-workflow-' + place + '-medal-label', align: 'right' }).inject(medalTextRow);
                			var medalTextCell = new Element('td', {
                                        	html: '',
                                        	colspan: 2,
                                        	id: 'UCPANG-workflow-' + place + '-medaltext-cell', 
                                        	'class': 'Said' 
                                	}).inject(medalTextRow);
					listMedals( {
						medalElement: $('UCPANG-workflow-' + place + '-list-players-cell'),
						medalPreviewElement: medalTextCell,
						loadedMedals: loadedMedals,
						idPrefix: 'UCPANG-workflow-' + place + '-',
					});
				}
			}
		    }
		}).inject(nthSelectCell);
		new Element('label', { 
			'for': 'UCPANG-workflow-' + place + '-awarding', 
			html: place == 'winner' ? 'winner(s)' : 'award ' + place + ' place/medal'
		}).inject(nthSelectCell);
		new Element('td', {
	    	    id: 'UCPANG-workflow-' + place + '-list-players-cell'
		}).inject(nthRow);
	    });

	    // simulate 'winners' click
	    $('UCPANG-workflow-winner-awarding').click();

	    if (groupConfig.workflow().stickyChallenges) {
		    var unstickyThreadRow = new Element('tr').inject(wizardDialog);
		    new Element('td', { html: 'remove&nbsp;stickiness:',
					id: 'UCPANG-workflow-wizard-unsticky-thread-label' }).inject(unstickyThreadRow);
		    var unstickyThreadCell = new Element('td').inject(unstickyThreadRow);
		    new Element('input', { type: 'checkbox', disabled: true, checked: true }).inject(unstickyThreadCell);
	    }
	    var buttonRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    var buttonCell = new Element('td', {
		colspan: 3,
		align: 'right',
		valign: 'top'
	    }).inject(buttonRow);
	    new Element('button', {
		'class': 'CancelButt',
		html: 'Cancel',
		events: {
			'click': function (evt) {
				$('UCPANG-promote-wizard-dialog').destroy();
			}
		}
	    }).inject(buttonCell);
	    new Element('span', {
		html: ' '
	    }).inject(buttonCell);
	    new Element('button', {
		'class': 'Butt',
		html: 'SAVE without a medal?',
		id: 'UCPANG-promote-wizard-save-button',
		styles: {
			'background-color': 'orange'
		},
		events: {
			'click': function (evt) {
				if ($('UCPANG-promote-wizard-save-button').hasClass('Butt')) {
					$('UCPANG-promote-wizard-save-button').set('class', 'DisabledButt');
					expectedRemoteActions.set('title', true);
					if (groupConfig.workflow().stickyChallenges) {
						expectedRemoteActions.set('unstick', true);
					}
					[ 'winner', 'second', 'third', 'fourth', 'fifth' ].each( function (place) {
						var idPrefix = 'UCPANG-workflow-' + place + '-';
						if ($(idPrefix + 'awarding').checked) {
							expectedRemoteActions.set(idPrefix + 'medal', true);
							expectedRemoteActions.set(idPrefix + 'tags', true);
							expectedRemoteActions.set(idPrefix + 'invite', true);
						}
					});

					// show spinning balls
					var button = $(evt.target);
					button.empty();
					button.adopt(new Element('img', {
						src: updatingIcon,
						title: 'updating challenge'
					}));
					var wizardDialog = $('UCPANG-promote-wizard-dialog');
					// create a panel where we can view the progress
					var unstickinessLabel = $('UCPANG-workflow-wizard-unsticky-thread-label');
					var unstickinessRow = unstickinessLabel.getParent('tr');
					var progressRow = new Element('tr').inject(unstickinessRow, 'after');
					var progressPanel = new Element('td', { id: 'UCPANG-workflow-wizard-progress-panel', colspan: 3 }).inject(progressRow);
					new Element('div', { html: 'actions progress:', styles: { fontWeight: 'bold' } }).inject(progressPanel);
					
					// update title
					createProgressElement('title update', 'UCPANG-workflow-progress-newtitle', function () {
						remoteActions.push(true);
						expectedRemoteActions.erase('title');
						updateChallengeTitle($('UCPANG-workflow-wizard-newtitle').get('text'), function (result) { // use 'text', not 'html'
							var button = $('UCPANG-workflow-progress-newtitle-icon');
							updateRemoteAction(result.success, 'error updating title' + result.msg, button, 'title updated');
						});
					}).fireEvent('run-action');
					
					[ 'winner', 'second', 'third', 'fourth', 'fifth' ].each( function (place) {
					  	var idPrefix = 'UCPANG-workflow-' + place + '-';
						var playerHash = winnersHashes[place];

						if (!$(idPrefix + 'awarding').checked) {
							expectedRemoteActions.erase(idPrefix + 'medal');
							expectedRemoteActions.erase(idPrefix + 'tags');
							expectedRemoteActions.erase(idPrefix + 'invite');
							return;
						}
						if (playerHash == undefined || playerHash.getLength() == 0) {
							// selected the checkbox on the outer left, but did not choose a player from the list
							remoteActions.push(true);
							new Element('img', { 
								src: errorIcon, 
								title: 'forgot to select a player in the list'
							}).inject($(idPrefix + 'medal-label'));
							expectedRemoteActions.erase(idPrefix + 'medal');
							expectedRemoteActions.erase(idPrefix + 'tags');
							expectedRemoteActions.erase(idPrefix + 'invite');

							updateRemoteAction(false, 'forgot to select a player in the list', button, undefined);
						}
						// award a medal
						var medalText = $(idPrefix + 'medaltext').get('html').replace(/\n/g, '').replace(/<br[^>]*>/g, '\n');
						if (medalText !== '--none--' && medalText.length > 0) {
							playerHash.forEach( function(value, key) {
								createProgressElement('posting medal \'' + place + '\' to \'<i>' + value.photoTitle() + '</i>\' from ' + value.poster().username, 'UCPANG-workflow-progress-medal-' + place + '-' + key, function () {
									remoteActions.push(true);
									expectedRemoteActions.erase(idPrefix + 'medal');
									postMedal(medalText, value, key, function (key, result) {
										var button = $('UCPANG-workflow-progress-medal-' + place + '-' + key + '-icon');
										updateRemoteAction(result.success, 'error posting medal: ' + result.msg, button, 'medal posted');
									});
								    }
								).fireEvent('run-action');
							});
						} else {
							expectedRemoteActions.erase(idPrefix + 'medal');
						}
						
						// add tags
						var tagText = $(idPrefix + 'tags').get('text'); // use 'text', not 'html'
						if (tagText !== '--none--' && tagText.length > 0) {
							playerHash.forEach( function(value, key) {
								createProgressElement('posting tags to \'<i>' + value.photoTitle() + '</i>\' from ' + value.poster().username, 'UCPANG-workflow-progress-tags-' + place + '-' + key, function () {
									remoteActions.push(true);
									expectedRemoteActions.erase(idPrefix + 'tags');
									postTags({
										key: key,
										tags: tagText,
										photo: value,
										callback: function (key, result) {
											var button = $('UCPANG-workflow-progress-tags-' + place + '-' + key + '-icon');
											updateRemoteAction(result.success, 'error adding tags: ' + result.msg, button, 'tags posted');
										}
									});
								    }
								).fireEvent('run-action');
							});
						} else {
							expectedRemoteActions.erase(idPrefix + 'tags');
						}
					
						// drop admin invites
						var invitesCell = $(idPrefix + 'invites');
						invitesCell.getElements('div').forEach( function (groupElement, idx) {
							var groupId = groupElement.get('id').replace('UCPANG-workflow-invitegroup-','');
							var wanted = groupElement.getElement('input[type=checkbox]').checked;
							if (wanted && groupId && groupId.length > 0 && groupId.match(/\d+@\w\d{2}/)) {
								playerHash.forEach( function(value, key) {
									createProgressElement('dropping photo \'<i>' + value.photoTitle() + '</i>\' from ' + value.poster().username + ' an admin invite for group <i>' + GM_getValue("UCPA.groupname." + groupId) + '</i>',
										'UCPANG-workflow-progress-invite-' + key + '_' + idx,
										function () {
											remoteActions.push(true);
											expectedRemoteActions.erase(idPrefix + 'invite');
											sendAdminInvite({
												key: key + '_' + idx,
												groupId: groupId,
												photo: value,
												callback: function (key, result) {
													var button = $('UCPANG-workflow-progress-invite-' + key + '-icon');
													updateRemoteAction(result.success, 'error inviting: ' + result.msg, button, 'invited');
												}
											});
										}
									).fireEvent('run-action');
								});
							}
						});
						expectedRemoteActions.erase(idPrefix + 'invite');
					});

					// un-sticky the thread
					if (groupConfig.workflow().stickyChallenges) {
						createProgressElement('remove stickiness from thread', 'UCPANG-workflow-progress-unsticky', function () {
							remoteActions.push(true);
							expectedRemoteActions.erase('unstick');
							unstickyChallengeThread(function (result) {
								var button = $('UCPANG-workflow-progress-unsticky-icon');
								updateRemoteAction(result.success, 'error removing stickiness: ' + result.msg, button, 'removed stickiness');
							});
						}).fireEvent('run-action');
					}
				}
			}
		}
	    }).inject(buttonCell);

	    // simulate a mouse click on the winners
	    ucpThread.photos().each( function (competitor, idx) {
	    	if ($chk(score) && score.options.votesArray[idx+1] == maxVote) {
	    		$('UCPANG-workflow-winner-' + idx).click();
		}
	    });
	    if (winnersHashes['winner'].getLength() > 1) {
	    	new Element('td', {
			html: 'found a tie!',
			styles: {
				color: 'red'
			}
		}).inject(new Element('tr').inject($('UCPANG-workflow-wizard-current-title-row'), 'after'));
	    }
	}
    }

/* Levenshtein distance function
Calculates the 'distance' between two strings.
Is used for marking themes that are in use when creating a new challenge.

Copied from http://webreflection.blogspot.com/2009/02/levenshtein-algorithm-revisited-25.html
*/
var levenshtein = function(min, split){
    // Levenshtein Algorithm Revisited - WebReflection
    try{split=!("0")[0]}catch(i){split=true};
    return function(a, b){
        if(a == b)return 0;
        if(!a.length || !b.length)return b.length || a.length;
        if(split){a = a.split("");b = b.split("")};
        var len1 = a.length + 1,
            len2 = b.length + 1,
            I = 0,
            i = 0,
            d = [[0]],
            c, j, J;
        while(++i < len2)
            d[0][i] = i;
        i = 0;
        while(++i < len1){
            J = j = 0;
            c = a[I];
            d[i] = [i];
            while(++j < len2){
                d[i][j] = min(d[I][j] + 1, d[i][J] + 1, d[I][J] + (c != b[J]));
                ++J;
            };
            ++I;
        };
        return d[len1 - 1][len2 - 1];
    }
}(Math.min, false);

function normalizeTheme(theme) {
	if (!$chk(theme)) return "";
	return theme.replace( /\/s/, '' )
		.replace(/\s/g, '')
		.replace(/\r/,"")
		.replace(/&amp;/g,"&")
		.replace(/^(the|a|an)\s+/i, '')
		.replace(/^in\s+/i, '')
		.replace(/\s+in\s+(the|a|an)\s+/i, '')
		.replace(/\s+in\s+/i, '')
		.replace(/\s+(the|a|an)\s+/i, '')
		.toLowerCase();
}

function markThemesInPlay(callback) {
	new Request({
		//async: false,
		url: /(.*flickr.com\/groups\/[^\/]+\/)/.exec(document.location.href)[1] + 'discuss/',
		onFailure: function (response) {
		    try {
			callback({ success: false, msg: response.statusText });
		    } catch (e) {
			GM_log("failed loading Discuss page: " + e);
		    }
		},
		onSuccess: function (responseText, responseXml) {
               		var tempDiv = new Element('div');
	                tempDiv.innerHTML = responseText.stripScripts();
	                var problem = tempDiv.getElement('p.Problem');
	                if ($chk(problem)) {
				callback({ success: false, msg: problem.innerHTML });
	                } else {
				var topicListing = tempDiv.getElement('table.TopicListing');
				if (!$chk(topicListing)) {
					callback({ success: false, msg: 'no topic listing found' });
					return;
				}
				var topics = topicListing.getElements('tr');
				var activeThemes = [];
				var markThemeExtract  = groupConfig.workflow().markThemeExtractRegExp;
				var themeExtracter = new RegExp(markThemeExtract.expression, markThemeExtract.flags);
				topics.forEach( function (row, idx) {
					if (idx == 0) return; // skip table header
					var titleCell = row.getElement('td');
					var title = titleCell.getElement('a').get('text');
					if (groupConfig.skipChallenge(title)) return;
					var theme = title.replace(
						themeExtracter, 
						markThemeExtract.replacement);
					theme = normalizeTheme(theme);
					activeThemes.include(theme);
				});
				$('UCPANG-workflow-wizard-theme-combo').getElements('option').forEach( function (option) {
					listedTheme = option.get('text');
					listedTheme = normalizeTheme(listedTheme);
					if (activeThemes.some(function (theme) { return groupPreferences.useLevenshteinToMarkThemesInUse() ? levenshtein(theme,listedTheme) <= groupPreferences.levenshteinDistanceInUsedThemes() :  (theme == listedTheme) })) {
						if (Browser.Engine.webkit) { // Google Chrome
							// apparently no support for <s>
							option.setStyle('background-color', 'red');
						} else {
							var optionTheme = option.get('html');
							option.empty();
							new Element('s', { html: optionTheme }).inject(option);
						}
						option.set('class', 'marked');
					}
				});
				callback({ success: true });
			}
		}
	}).get();	
}
					
function pickRandomTheme() {
	var themeCombo = $('UCPANG-workflow-wizard-theme-combo');
	var themeList = themeCombo.getElements('option').filter(function (option) {
		return !option.hasClass('marked');
	});
	var randomIdx = $random(1,themeList.length);
	var randomTheme = themeList[randomIdx - 1];
	randomTheme.set('selected', true);
	$('UCPANG-workflow-wizard-theme-combo').fireEvent('change');
}

    function toggleAwardedToNewWizardDialog(ucpThread) {
	if (SIMULATE) alert("simulating");
	if ($chk($('UCPANG-promote-wizard-dialog'))) {
		$('UCPANG-promote-wizard-dialog').destroy();
	} else {
	    remoteActions = [];
	    failedRemoteActions = [];

            wizardDialog = new Element('table', {
		border: 0,
		id: 'UCPANG-promote-wizard-dialog',
                styles: ucpDialogStyle
	    }).inject($('UCPANG-workflow-cell'), 'after');

	    var titles = provideTitleChangeRow( {
	    	table: wizardDialog,
	    	titleReplacer: groupConfig.workflow().fromClosedToNewRegExp,
		nextStepChecker: groupConfig.workflow().fromOpenToVoteRegExp
	    });
	    var currentTitle = titles.currentTitle;
	    var newTitle = titles.newTitle;
	    var themeExtractRegExp = groupConfig.workflow().themeExtractRegExp;
	    var themeRegExp = new RegExp(themeExtractRegExp.expression, themeExtractRegExp.flags);
	    //alert("DEBUG: currentTitle='" + currentTitle + "' - regexp='" + themeRegExp + "' - match='" + currentTitle.match(themeRegExp) + "'");
	    if (currentTitle.match(themeRegExp)) {
	    	var currentTheme = currentTitle.replace(themeRegExp, themeExtractRegExp.replacement);
		$('UCPANG-workflow-wizard-newtitle').fireEvent('update-theme', currentTheme);
	    }
	    var headersRow = new Element('tr').inject(wizardDialog);
	    new Element('td', { html: 'header: ' }).inject(headersRow);
	    if ($chk(groupConfig.workflow().headersThread)) {
	    	var loadingHeadersImg = new Element('img', { src: updatingIcon, title: 'loading headers..' }).inject(new Element('td').inject(headersRow));
		var loadingHeadersText = new Element('i', { html: ' loading headers..' }).inject(loadingHeadersImg, 'after');
		var loadedHeaders = loadHeaders();
		loadingHeadersImg.getParent('td').dispose();
		if (loadedHeaders.success == true) {
			var headersCell = new Element('td').inject(headersRow);
			var headersCombo = new Element('select', {
				id: 'UCPANG-workflow-wizard-header-combo',
				events: {
					change: function (evt) {
						var selectedHeader = $(evt.target).getSelected().get('value');
						if (selectedHeader > -1) {
							// clean up everything
							// this will prevent all kinds of problems:
							// 1. one selects a header with %previous% placeholder(s)
							// 2. the winner is selected, and placeholder is replaced
							// 3. another header is selected, with %previous% placeholder(s)
							// => %previous% placeholder(s) should be filled in
							// with a clean start, no such problems :-)
							if ($chk($('UCPANG-workflow-wizard-replacement-row'))) {
								$('UCPANG-workflow-wizard-replacement-row').dispose();
							}
							[ 'third', 'second', 'winner' ].each(function (place) {
								if ($chk($('UCPANG-workflow-' + place + '-row'))) {
									$('UCPANG-workflow-' + place + '-row').dispose();
									$$('tr.UCPANG-workflow-' + place + '-list-players-row').dispose();
								}
							});
							$('UCPANG-workflow-wizard-theme-combo').getChildren('option')[0].set('selected', true);
							$('UCPANG-workflow-wizard-newheader').set('html', loadedHeaders.headers[selectedHeader].header);
							// does it contain %__previous templates?
							if (loadedHeaders.headers[selectedHeader].header.match('%__previous')) {
							    var replacementRow = new Element('tr', {
									id: 'UCPANG-workflow-wizard-replacement-row'
							    }).inject($('UCPANG-workflow-wizard-newheader').getParent('tr'),'after');
							    replacementRow.adopt(
								new Element('td'),
								new Element('td', { html: 'select a player to replace the <i>%__previous</i>... placeholders with:' })
							    );
							    [ 'third', 'second', 'winner' ].each(function (place) {
							    	var camel = place.charAt(0).toUpperCase() + place.replace(/^.(.*)$/, '$1');
							    	if (loadedHeaders.headers[selectedHeader].header.match('%__previous' + camel)) {
							            if (!$chk($('UCPANG-workflow-' + place + '-row'))) {
								    	var placeRow = new Element('tr', { 
								    	    id: 'UCPANG-workflow-' + place + '-row',
									    valign: 'top'
								    	}).inject(replacementRow, 'after');
							    	        new Element('td', {
										html: camel + ':'
							    	        }).inject(placeRow);
							   	    	var playersCell = new Element('td', {
									    id: 'UCPANG-workflow-' + place + '-'
							    	    	}).inject(placeRow);
									if (!winnersHashes[place]) winnersHashes[place] = new Hash();
									listPlayers( {
									    playersElement: playersCell,
									    playerHash: winnersHashes[place],
									    ucpThread: ucpThread,
									    idPrefix: 'UCPANG-workflow-' + place + '-',
									    radio: true,
								    	    winnerChangeCallback: function (winnerHash) {
										// start with clean header
										var selectedHeader = $('UCPANG-workflow-wizard-header-combo').getSelected().get('value');
										var newHeader = loadedHeaders.headers[selectedHeader].header;
										[ 'winner', 'second', 'third' ].each(function (p) {
							    			    var c = p.charAt(0).toUpperCase() + p.replace(/^.(.*)$/, '$1');
									    	    // radio button => winnerHash only contains 1 player
										    if (newHeader.match('%__previous' + c)) {
										        if (winnersHashes[p]) {
										    	    var winner; winnersHashes[p].forEach(function(player) { winner = player });
										        }
										        if (!winner) return;
											newHeader = newHeader.replace(new RegExp('%__previous' + c + '__name__%', 'g'), 
													winner.poster().username)
												 .replace(new RegExp('%__previous' + c + '__title__%', 'g'), 
												 	$chk(winner.photoTitle()) ? winner.photoTitle() : "<i>--untitled--</i>")
												 .replace(new RegExp('%__previous' + c + '__photo__%', 'g'),
												 	winner.photo().getParent('a').get('href'));
										    }
										});
										$('UCPANG-workflow-wizard-newheader').set('html', newHeader);
									    }
	    							    	});
								    }
								}
							    });
							}
							var dedicatedTheme = loadedHeaders.headers[selectedHeader].theme;
							var fixedTheme = false;
							if ($chk(dedicatedTheme)) { // overriding theme
								fixedTheme = true;
								if (dedicatedTheme.match('%__number__%')) {
									var numberExtract = groupConfig.workflow().numberExtractRegExp;
									if ($chk(numberExtract)) {
										var number = currentTitle.replace(
										    new RegExp(numberExtract.expression, numberExtract.flags), 
										    numberExtract.replacement);
										dedicatedTheme = dedicatedTheme.replace(/%__number__%/g, number);
									}
								}
								$('UCPANG-workflow-wizard-newtitle').set('html', dedicatedTheme);
							} else {
								$('UCPANG-workflow-wizard-newtitle').set('html', $('UCPANG-workflow-wizard-hiddentitle').get('value'));
								if ($chk(currentTheme)) {
									$('UCPANG-workflow-wizard-newtitle').fireEvent('update-theme', currentTheme);
	    							}
							}
							$('UCPANG-workflow-wizard-theme-combo').set('disabled', fixedTheme);
							$('UCPANG-workflow-wizard-theme-combo').setStyle('background-color', fixedTheme ? 'grey' : ''); // make it more visible in Chrome that this combo is disabled
							$('UCPANG-promote-wizard-save-button').setStyle('background-color', '');
							$('UCPANG-promote-wizard-save-button').set('html', 'SAVE');
							if ($chk($('UCPANG-promote-wizard-mark-themes-in-play'))) {
								$('UCPANG-promote-wizard-mark-themes-in-play').set('class', fixedTheme ? 'DisabledButt' : 'CancelButt');
							}
							if ($chk($('UCPANG-promote-wizard-pick-random-theme'))) {
								$('UCPANG-promote-wizard-pick-random-theme').set('class', fixedTheme ? 'DisabledButt' : 'CancelButt');
							}
						} else {
							$('UCPANG-workflow-wizard-newheader').set('html', '--none--');
							$('UCPANG-workflow-wizard-theme-combo').set('disabled', false);
							$('UCPANG-workflow-wizard-theme-combo').setStyle('background-color', '');
							$('UCPANG-workflow-wizard-theme-combo').getChildren('option')[0].set('selected', true);
							$('UCPANG-promote-wizard-save-button').setStyle('background-color', 'orange');
							$('UCPANG-promote-wizard-save-button').set('html', 'SAVE without a header?');
							if ($chk(currentTheme)) {
								$('UCPANG-workflow-wizard-newtitle').fireEvent('update-theme', currentTheme);
	    						}
						}
					} // end change event
				} // end events
			}).inject(headersCell);
			new Element('option', {
				value: -1,
				html: '-- select a header --'
			}).inject(headersCombo);
			loadedHeaders.headers.forEach( function(header, idx) {
				new Element('option', {
					value: idx,
					html: header.title
				}).inject(headersCombo);
			});
		} else {
			new Element('td', { html: '<i>' + loadedHeaders.error + '</i>' }).inject(headersRow);
		}
	    } else {
		new Element('td', { html: '<i>no header list known - contact Alesa Dam for support</i>' }).inject(headersRow); // TODO: provide a means to notify Alesa Dam
	    }

	    var themesRow = new Element('tr').inject(wizardDialog);
	    new Element('td', { html: 'theme: ' }).inject(themesRow);
	    if ($chk(groupConfig.workflow().themesThread)) {
	    	var loadingThemesImg = new Element('img', { src: updatingIcon, title: 'loading themes..' }).inject(new Element('td').inject(themesRow));
		var loadingThemesLabel = new Element('i', { html: ' loading themes..' }).inject(loadingThemesImg, 'after');
	    	var loadedThemes = loadThemes();
		loadingThemesImg.getParent('td').dispose();
		if (loadedThemes.success == true) {
			var themesCell = new Element('td').inject(themesRow);
			var hiddenTitle = new Element('input', { type: 'hidden', id: 'UCPANG-workflow-wizard-hiddentitle', value: newTitle }).inject(themesCell);
			var themeCombo = new Element('select', {
				id: 'UCPANG-workflow-wizard-theme-combo',
				events: {
					change: function (evt) {
					    var selectedTheme = $('UCPANG-workflow-wizard-theme-combo').getSelected().get('value');
					    if (isNaN(selectedTheme)) {
					    	$('UCPANG-workflow-wizard-newtitle').fireEvent('update-theme', selectedTheme);
					    } else {
					    	if ($chk(currentTheme)) {
					    		$('UCPANG-workflow-wizard-newtitle').fireEvent('update-theme', currentTheme);
						}
					    }
					}
				}
			}).inject(themesCell);
			new Element('option', {
				value: -2,
				html: '-- select a theme --'
			}).inject(themeCombo);
			var themeCategoryRegExp = groupConfig.workflow().themeCategoryRegExp;
			if ($chk(themeCategoryRegExp)) {
				var themeCategoryMatcher = new RegExp(themeCategoryRegExp.expression, themeCategoryRegExp.flags);
			}
			var optgroup = undefined;
			loadedThemes.themes.forEach( function(theme, idx) {
				if (theme.length == 0) return; // skip empty lines
				if ($chk(themeCategoryMatcher) && theme.match(themeCategoryMatcher)) {
					optgroup = new Element('optgroup', {
						label: theme,
						styles: {
							backgroundColor: 'yellow'
						}
					}).inject(themeCombo);
				} else {
					new Element('option', {
						html: theme.replace(/<[^>]+>/g, ''), // remove any html formatting in the list - don't do this before matching!
						styles: {
							backgroundColor: 'white'
						}
					}).inject($chk(optgroup) ? optgroup : themeCombo);
				}
			});
			var markThemeExtract  = groupConfig.workflow().markThemeExtractRegExp;
			if ($chk(markThemeExtract)) {
			    new Element('img', {
				id: 'UCPANG-promote-wizard-mark-themes-in-play',
				src: images.mark,
				'class': 'CancelButt',
				title: 'mark the themes that are currently in play',
				styles: {
					height: '14px'
				},
				events: {
					click: function (evt) {
						markButton = $('UCPANG-promote-wizard-mark-themes-in-play');
						if (markButton.hasClass('DisabledButt')) return; // still busy
						markButton.set('src', updatingIcon);
						markButton.set('title', 'loading Discuss page');
						markButton.set('class', 'DisabledButt');
						var pickRandom = $('UCPANG-promote-wizard-pick-random-theme');
						if ($chk(pickRandom)) pickRandom.set('class', 'DisabledButt');
						markThemesInPlay(function (result) {
							var success = result.success;
							var msg = result.msg;
							markButton.set('src', success ? defaultCheckIconSmall : errorIcon);
							markButton.set('title', success ? 'marked themes in play' : 'error: ' + msg);
							markButton.set('class', 'CancelButt');
							var pickRandom = $('UCPANG-promote-wizard-pick-random-theme');
							if ($chk(pickRandom)) pickRandom.set('class', 'CancelButt');
						});
					}
				}
			    }).inject(themesCell);
			}
			new Element('span', { html: ' ' }).inject(themesCell);
			new Element('img', {
				id: 'UCPANG-promote-wizard-pick-random-theme',
				src: images.random,
				'class': 'CancelButt',
				title: 'pick random theme',
				styles: {
					height: '14px'
				},
				events: {
					click: function (evt) {
						if ($('UCPANG-promote-wizard-pick-random-theme').hasClass('DisabledButt')) return;
						var markButton = $('UCPANG-promote-wizard-mark-themes-in-play');
						if ($chk(markButton) && !markButton.hasClass('DisabledButt')) {
							markButton.set('src', updatingIcon);
							markButton.set('title', 'loading Discuss page');
							markButton.set('class', 'DisabledButt');
							markThemesInPlay(function (result) {
								var success = result.success;
								var msg = result.msg;
								markButton.set('src', success ? defaultCheckIconSmall : errorIcon);
								markButton.set('title', success ? 'marked themes in play' : 'error: ' + msg);
								pickRandomTheme();
							});
						} else {
							pickRandomTheme();
						}
					}
				}
			}).inject(themesCell);
		} else {
			new Element('td', { html: loadedThemes.error }).inject(themesRow);
		}
	    } else {
	    	new Element('td', { html: '<i>no theme list known - contact Alesa Dam for support</i>' }).inject(themesRow); // TODO: provide a means to notify Alesa Dam
	    }

	    // move the new title row below the header and theme combos
	    $('UCPANG-workflow-wizard-new-title-row').inject(themesRow, 'after');
	    // change 'new title:' into 'preview'
	    $('UCPANG-workflow-wizard-new-title-label').set('html', 'preview:');
	    // move the new title into its own row and make it look like a title
	    var newTitleRow = new Element('tr').inject($('UCPANG-workflow-wizard-new-title-row'), 'after');
	    $('UCPANG-workflow-wizard-new-title-cell').inject(newTitleRow);
	    var header = new Element('h2');//.inject(wizardDialog); // temporary, or else it won't work
	    $('UCPANG-workflow-wizard-new-title-cell').getChildren().each(function (el) {
	    	el.inject(header);
	    });
	    header.inject('UCPANG-workflow-wizard-new-title-cell');


	    var newHeaderRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    var newHeaderCell = new Element('td', { 'class': 'Said', colspan: 3 } ).inject(newHeaderRow);
	    createEditableTextElement(newHeaderCell, 'UCPANG-workflow-wizard-newheader', 'white', null);

	    if (groupConfig.workflow().stickyChallenges) {
		    var stickyRow = new Element('tr').inject(wizardDialog);
		    new Element('td', { html: 'make sticky', id: 'UCPANG-workflow-wizard-sticky-label' }).inject(stickyRow);
		    var stickyCell = new Element('td').inject(stickyRow);
		    new Element('input', {
			type: 'checkbox',
			checked: true,
			id: 'UCPANG-workflow-wizard-sticky-check'
		    }).inject(stickyCell);
	    }
	    var buttonRow = new Element('tr', { valign: 'top' }).inject(wizardDialog);
	    var buttonCell = new Element('td', {
		colspan: 2,
		align: 'right',
		valign: 'top'
	    }).inject(buttonRow);
	    new Element('button', {
		'class': 'CancelButt',
		html: 'Cancel',
		events: {
			'click': function (evt) {
				$('UCPANG-promote-wizard-dialog').destroy();
			}
		}
	    }).inject(buttonCell);
	    new Element('span', {
		html: ' '
	    }).inject(buttonCell);
	    new Element('button', {
		'class': 'Butt',
		html: 'SAVE without a header?',
		styles: {
			backgroundColor: 'orange'
		},
		id: 'UCPANG-promote-wizard-save-button',
		events: {
			'click': function (evt) {
				if ($('UCPANG-promote-wizard-save-button').hasClass('Butt')) {
					// show spinning balls
					var button = $(evt.target);
					button.empty();
					button.adopt(new Element('img', {
						src: updatingIcon,
						title: 'creating challenge'
					}));
					var wizardDialog = $('UCPANG-promote-wizard-dialog');
					// create challenge
					var progressRow = new Element('tr').inject(buttonRow, 'after');
					var progressPanel = new Element('td', { id: 'UCPANG-workflow-wizard-progress-panel', colspan: 3 }).inject(progressRow);
					new Element('div', { html: 'actions progress:', styles: { fontWeight: 'bold' } }).inject(progressPanel);
					
					var allright = true;
					var threadId;
					createProgressElement('creating challenge', 'UCPANG-workflow-progress-new-challenge', function () {
						createChallenge(
							$('UCPANG-workflow-wizard-newtitle').get('text'), // use 'text', not 'html'
							$('UCPANG-workflow-wizard-newheader').get('html').replace(/\n/g, '').replace(/<br[^>]*>/g, '\n'),
							function (retval) {
								var success = retval.success;
								if (!success) {
									allright = false;
									$('UCPANG-workflow-progress-new-challenge-icon').set('src', errorIcon);
									$('UCPANG-workflow-progress-new-challenge-icon').set('title', retval.error);
									return;
								}
								$('UCPANG-workflow-progress-new-challenge-icon').set('src', defaultCheckIconSmall);
								$('UCPANG-workflow-progress-new-challenge-icon').set('title', 'challenge created');
								threadId = retval.threadId;
								if (groupConfig.workflow().stickyChallenges && $('UCPANG-workflow-wizard-sticky-check').checked) {
									createProgressElement('making challenge sticky',
												'UCPANG-workflow-progress-make-sticky',
												function () {
										stickifyChallengeThread(threadId, function (retval) {
											var success = retval.success;
											if (!success) {
												allright = false;
												$('UCPANG-workflow-progress-make-sticky-icon').set('src', errorIcon);
												$('UCPANG-workflow-progress-make-sticky-icon').set('title', retval.msg);
											} else {
												$('UCPANG-workflow-progress-make-sticky-icon').set('src', defaultCheckIconSmall);
												$('UCPANG-workflow-progress-make-sticky-icon').set('title', 'challenge made sticky');
											}
										});
												}
									).fireEvent('run-action');
								}
							}
						);
					}).fireEvent('run-action');

					button.getElement('img').set('src', allright ? defaultCheckIconSmall : errorIcon);
					if ($chk(threadId)) {
						var newThreadRow = new Element('tr').inject(wizardDialog);
						new Element('td', { html: 'created thread:' }).inject(newThreadRow);
						new Element('a', {
							html: $('UCPANG-workflow-wizard-newtitle').get('text'), // use 'text', not 'html'
							href: /(.*flickr.com\/groups\/[^\/]+\/discuss\/)/.exec(document.location.href)[1] + threadId
						}).inject(new Element('td').inject(newThreadRow));
						if (allright) {
                                                        document.location.href = /(.*flickr.com\/groups\/[^\/]+\/discuss\/)/.exec(document.location.href)[1] + threadId;
                                                }

					}
				}
			}
		}
	    }).inject(buttonCell);

	}
    }

    function createChallenge(title, header, callback) {
	if (SIMULATE) {
		callback({
			success: false,
			error: 'simulating'
		});
		return;
	}
        var message = header.replace(/^\s*<p[^>]*>/,'');
        new Request({
            method: "post",
            url: 'http://www.flickr.com/groups_newtopic.gne',
	    async: false,
            headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Referer": 'http://www.flickr.com/groups_newtopic.gne?id=' + groupConfig.groupId()
            },
            data: "magic_cookie=" + GM_getAuthHash() + 
	    	  "&id=" + groupConfig.groupId() +
                  "&done=1" +
                  "&subject=" + encodeURIComponent(title) +
                  "&message=" + encodeURIComponent(message),
            onFailure: function (response) {
                GM_log("error creating challenge: " + response.statusText);
		callback({
			success: false, 
			error: response.statusText
		});
            },
            onSuccess: function (responseText, responseXML) {
                var tempDiv = new Element('div');
                tempDiv.innerHTML = responseText.stripScripts();
                var problem = tempDiv.getElement('p.Problem');
                if ($chk(problem)) {
                	callback({
				success: false,
				error: problem.innerHTML
			});
                } else {
			// the newly created challenge contains a permalink to itself
			var topicId = /\/groups\/[^\/]+\/discuss\/(\d+)/.exec(tempDiv.getElement('td.Said').getElement('small').getElement('a'))[1];
			callback({
				success: true,
				threadId: topicId
			});
		}
            }
        }).send();
    }

    function updateRemoteAction(result, msg, buttonImg, title) {
	if (result) {
		remoteActions.pop();
		buttonImg.set('src', defaultCheckIconSmall);
		buttonImg.set('title', title);
	} else {
		failedRemoteActions.push(true); // push to failures, before popping from active
		remoteActions.pop();
		buttonImg.set('src', errorIcon);
		buttonImg.set('title', msg);
	}
	if (remoteActions.length == 0 && expectedRemoteActions.getLength() == 0) {
	    if (failedRemoteActions.length == 0) {
       		if (!SIMULATE) document.location.href = /(.*flickr.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(document.location.href)[1] + '/';
	    } else {
	        var saveButton = $('UCPANG-promote-wizard-save-button').getElement('img');
		saveButton.set('src', errorIcon);
		$('UCPANG-promote-wizard-save-button').set('title', 'hover the red cross(es) to get individual error reports');
	    }
	    new Element('a', {
		href: /(.*flickr.com\/groups\/[^\/]+\/discuss\/\d+)/.exec(document.location.href)[1] + '/',
		html: 'reload page to go to next step'
	    }).inject(new Element('td', {
		colspan: 3
	    }).inject(new Element('tr').inject($('UCPANG-promote-wizard-save-button').getParent('tr'), 'after')));
	}
    }

    function postMedal(medalText, photoData, key, callback) {
    	if (SIMULATE) {
		// flickr.photos.comments.addComment call requires authenticated key
		callback(key, { success: true });
		return;
	}
	var apiData = {
        	    api_key: GM_getSessionKey(),
	            auth_hash: GM_getAuthHash(),
        	    auth_token: GM_getAuthToken(),
	            format: 'json',
        	    nojsoncallback: 1,
	            method: 'flickr.photos.comments.addComment',
		    photo_id: photoData.photoId(),
		    comment_text: medalText
        	};
	new Request({
        	    url: "http://www.flickr.com/",
	            onFailure: function (response) {
        	        callback(key, { success: false, msg: response.statusText });
	            },
        	    onSuccess: function (responseText, responseXML) {
                	var result;
	                try {
        	            result = JSON.parse(responseText);
                	} catch (e) {
	                    result = eval('(' + responseText + ')');
        	        }
                	if (result.stat === 'fail') {
	                    callback(key, { success: false, msg: result.code + " - " + result.message });
                	} else {
			    callback(key, { success: true });
			}
	            }
        }).get("/services/rest", apiData);
    }

    function postTags(data) {
	var key = data.key;
	var tags = data.tags;
	var photo = data.photo;
	var callback = data.callback;

    	if (SIMULATE) {
		// flickr.photos.addTags call requires authenticated key
		callback(key, { success: true });
		return;
	}

	// TODO: check if we can tag this photo
	// if not, and there is an tagThread, post it there instead of erroring out
	var apiData = {
            api_key: GM_getSessionKey(),
	    auth_hash: GM_getAuthHash(),
            auth_token: GM_getAuthToken(),
	    format: 'json',
            nojsoncallback: 1,
	    method: 'flickr.photos.addTags',
	    photo_id: photo.photoId(),
	    tags: tags
        };
	new Request({
            url: "http://www.flickr.com/",
	    onFailure: function (response) {
                callback(key, { success: false, msg: response.statusText });
	    },
            onSuccess: function (responseText, responseXML) {
              	var result;
	        try {
                    result = JSON.parse(responseText);
              	} catch (e) {
	            result = eval('(' + responseText + ')');
                }
               	if (result.stat === 'fail') {
	            callback(key, { success: false, msg: result.code + " - " + result.message });
               	} else {
		    callback(key, { success: true });
		}
	    }
        }).get("/services/rest", apiData);
    }

    function sendAdminInvite(data) {
    	var key = data.key;
	var groupId = data.groupId;
	var photo = data.photo;
	var photoId = photo.photoId();
	var callback = data.callback;
	var force = data.force;

    	if (SIMULATE) {
		callback(key, { success: true });
		return;
	}
	var groupAdminOrMod = isGroupAdministratorOrModerator(userNsid, groupId);
	if (groupAdminOrMod.administrator || force) {
          var apiData = {
            api_key: GM_getSessionKey(),
            auth_hash: GM_getAuthHash(),
            auth_token: GM_getAuthToken(),
            format: 'json',
            nojsoncallback: 1,
            method: 'flickr.groups.invite.photo.invite',
            group_id: groupId,
	    photo_id: photoId
          };
          new Request({
            url: "http://www.flickr.com/",
            onFailure: function (response) {
                callback(key, { success: false, msg: response.statusText });
            },
            onSuccess: function (responseText, responseXML) {
                var result;
                try {
                    result = JSON.parse(responseText);
                } catch (e) {
                    result = eval('(' + responseText + ')');
                }
                if (result.stat === 'fail') {
                    callback(key, { success: false, msg: result.code + " - " + result.message });
                } else {
		    callback(key, { success: true });
		}
            }
          }).get("/services/rest", apiData);
	} else if ($chk(loadedMedals.inviteThread)) {
    		postThreadReply({
    			threadUrl: loadedMedals.inviteThread,
			message: "[http://www.flickr.com/photos/" + photo.owner().userid + "/" + photoId + "]\n" +
				 "has won challenge " + document.location.href + "\n" +
				 "and needs an invite for group '" + groupAdminOrMod.groupname + "'",
			callback: function(retval) {
				callback(key, { success: retval.success, msg: retval.msg });
			}
		});
	} else {
		data.force = true;
		sendAdminInvite(data);
	}
    }

    function getThumbDimensions(photoId) {
    	var retval = { width:0, height:0 };
        var magisterLudi = GM_getMagisterLudi();
        var apiData = {
            api_key: magisterLudi,
            auth_hash: GM_getAuthHash(),
            auth_token: GM_getAuthToken(),
            format: 'json',
            nojsoncallback: 1,
            method: 'flickr.photos.getSizes',
            photo_id: photoId
        };
        new Request({
            url: "http://www.flickr.com/",
            async: false,
            onFailure: function (response) {
                GM_log("error calling flickr.photos.getSizes: " + response.statusText);
            },
            onSuccess: function (responseText, responseXML) {
                var result;
                try {
                    result = JSON.parse(responseText);
                } catch (e) {
                    result = eval('(' + responseText + ')');
                }
                if (result.stat === 'fail') {
                    GM_log("failed parsing photo sizes reply: " +result.code + " - " + result.message);
                    return;
                }
                var sizes = result.sizes;
                $each(sizes.size, function (size) {
                    if (size.label == 'Thumbnail') {
		    	retval = { width: parseInt(size.width), height: parseInt(size.height) };
                    }
                });
            }
        }).get("/services/rest", apiData);
	return retval;
    }

    function processPendingItems() {
        var buttons = [];
        $$('td.gPendPic').set('rowspan', 3);
        $$('img').filter(ucpCheckPendingPhoto).each(function (photo) {
            var photoCell = photo.getParent('td.gPendPic');
            photoCell.rowSpan = 2;
            var photoRow = photo.getParent('tr');
            var awardsRow = new Element('tr').inject(photoRow, 'after');
            var awardsCell = new Element('td').inject(awardsRow);
            var comboRow = new Element('tr').inject(awardsRow, 'after');
            var allAwardsCell = new Element('td', { colspan: 2 }).inject(comboRow);
            var photoId = photo.get('src').match(/https?:\/\/.*flickr.com\/\d+\/(\d+)_.*/)[1];
            var allAwardsButton = addAwardsCombo(allAwardsCell, 
                "awards in group", 
                awardsCell,
                groupPreferences.defaultAwardsGroupInPendingItems(),
                'flickr.photos.comments.getList', 
                { photo_id: photoId },
                allAwardsCallback
            );
            buttons.include(allAwardsButton);
        });
        new Element('table', {
            border: '0',
            width: '100%',
            cellPadding: '0',
            cellSpacing: '0'
        }).adopt(new Element('tr', { styles: { height: '1.02em' } }).adopt(
            new Element('td', {
                align: 'right',
                vAling: 'center',
                width: '99%',
                height: '1.02em'
            }).adopt(
                new Element('button', {
                    html: 'check awards',
                    'class': 'CancelButt',
                    events: {
                        'click': function (event) {
                            event.preventDefault(); // click would trigger the form 'submit'
                            buttons.each(function (button) {
                                button.click();
                            });
                        }
                    }
                })
            ),
            new Element('td', {
                html: "&nbsp;&nbsp;",
                height: '1.02em'
            }),
            new Element('td', {
                id: 'UCPANG.gPendAll',
                nowrap: 'nowrap',
                align: 'right',
                vAlign: 'center',
                height: '1.02em',
                styles: {
                    whiteSpace :'nowrap',
                    overflow: 'hidden',
                }
            })
        )).inject($$('p.gPendAll')[0], 'before');
        $('UCPANG.gPendAll').grab($$('p.gPendAll')[0]);
    }

//the real code
// *******************
// Start of processing
// *******************

    if (window.name === 'Log page') {
        return; //don't process log page
    }

    var storedVersion;
    if (!$chk(GM_getValue("UCPA.version"))) {
        cleanupUCPAvariables();
    } else {
        storedVersion = GM_getValue("UCPA.version");
        if (CPtoolversion !== storedVersion) {
            cleanupUCPAvariables();
        }
    }
    GM_setValue("UCPA.version", CPtoolversion);

    // special case: iframe with meta data from userscripts.org
    if (document.location.href.match('http://userscripts.org/scripts/source/' + scriptNumber + '.meta.js')) {
    GM_log("storing version info");
        storeVersion();
        return;
    }

    checkVersion();

    var discussPage = false;
    var threadPage = false;
    var pendingItemsPage = false;
    var groupNavigationMenu;
    var supportedGroupOrPage = false;
    if (document.location.href.match(/.*\/edit\/?/)) {
        GM_log("not processing edit page");
        return;
    } else if (document.location.href.match(/.*flickr.com\/groups\/.*\/discuss(?:\/page\d+)?(?:\/)?$/)) {
        GM_log("on discuss page");
        discussPage = true;
        groupNavigationMenu = $('cattington_outer');
        var navigation = $$('div#Main table#SubNav tbody tr').getElement('td.Section');
        supportedGroupOrPage = initialize();
    } else if (document.location.href.match(/.*flickr.com\/groups\/.*\/discuss\/\d+/)) {//(?:\/page\d+)?(?:\/)?$/)) {
        GM_log("on challenge thread page");
        threadPage = true;
        var navigation = $$('div#Main h1#Tertiary');
        supportedGroupOrPage = initialize();
    } else if (document.location.href.match(/.*flickr.com\/groups\/.*\/admin\/pending\//)) {
        GM_log("on pending stuff page");
        pendingItemsPage = true;
        var navigation = $$('div#Main table#SubNav tbody tr').getElement('td.Section');
        supportedGroupOrPage = initialize();
    }

    if (!supportedGroupOrPage) {
        return;
    }

    var isAdminOrMod = isGroupAdministratorOrModerator(userNsid);
    if (!isAdminOrMod.moderator && !isAdminOrMod.administrator) {
        GM_debug("you are not an admin/mod for this group - aborting");
        return;
    }

    if (discussPage) {
        if (challengeGroup) {
            groupNavigationMenu.adopt(
            new Element('p', {
                'class': 'LinksNewP',
                id: 'UCheckPlayNGAdminPreferences'
            })).adopt(preferencesSpan = new Element('span', {
                    styles: {
                        fontWeight: 'bold'
                    },
                    html: "UCheckPlayNG Admin preferences:"
                })).adopt(
                new Element('a', {
                    name: "#",
                    title: "Click to change preferences",
                    styles: {
                        color: "Blue",
                        cursor: "pointer"
                    },
                    html: " [ " +
                        (groupPreferences.smartPhotoBoxes() ? "Smart info boxes - " : "") +
                        (groupPreferences.hideNonWonMedals() ? "Hide non-won medals" : "Show non-won medals") + " - " +
                        (groupPreferences.bumpAndReload() ? "Bump & Reload" : "Bump") +
                        " ]",
                    events: {
                        'click': togglePreferencesDialog
                    }
                })
            );
        }
        if (topicListingTable) {
            processTopicListingTable(topicListingTable);
        } else {
            GM_log("not processing (no TopicListing found!)");
        }
    } else if (threadPage && challengeGroup) { // challenge thread page
        var discussionTopic = $('DiscussTopic');
        var challengeHeader = $('Main').getElement('td#GoodStuff h2');
        var chlgname = challengeHeader.innerHTML;
        var thread   = document.location.href;
        var ucpThread = ucpCreateChallengeThread({
            groupConfig: groupConfig,
            url: thread, 
            chlgname: chlgname,
            groupConfig: groupConfig,
	    replyDecoratorFactory: new UCPEmptyDecoratorFactory(),
	    decoratorFactory: new UCPWorkflowThreadDecoratorFactory()
        });
        ucpThread.retrieve();
        ucpThread.setLastLoadTime(Math.round(CPStartTime.getTime() / 1000));
        ucpThread.setChallengeName(chlgname);
	if ($chk(groupConfig.workflow())) {
	    var workflowAvailable = false;
	    if (groupConfig.workflow().debug == true) {
		// when still debugging, only allow certain appointed people to help debugging
		var loggedInUserId = GM_getGlobalNsid();
		workflowAvailable = groupConfig.workflow().testers.some( function (tester) {
			return loggedInUserId == tester;
		});
	    } else {
		workflowAvailable = groupConfig.workflow().debug == undefined || groupConfig.workflow().debug == false;
	    }
	    if (workflowAvailable) {
        	try {
	            var challengeAnnouncementCell = $(discussionTopic).getElement('td.Said');
        	} catch (e) {
	            GM_log("error: " + e);
        	    // ignore
        	}
	        if (!$chk(challengeAnnouncementCell) || challengeAnnouncementCell.lenght === 0) {
        	    GM_log("no announcement");
	            ucpThread.setChallengeStatus("ERRORPARSING");
        	    ucpThread.store();
	            ucpThread.printStatus();
        	    ucpWorkflowCell.adopt(new Element('p', { html: 'no announcement found!?' }));
		    return;
        	}
		if (ucpThread.photos().length == 0) {
		        var challengeEntries = $(discussionTopic).getElements('td.Said');
    			ucpThread.challengeDefinition().readChallengeDefinitionOverrides(challengeAnnouncementCell);
        		ucpThread.collectVotes(challengeAnnouncementCell, challengeEntries);
		}
		// reset status
		ucpThread.setChallengeStatus("none");
        	var votes  = ucpThread.votes();
	        var comments = ucpThread.comments();
        	var photos = ucpThread.photos();
		try {
        		ucpProcessVotes(ucpThread);
		} catch (e) {
			// throws exception in non-challenge threads
		}
		new Element('button', {
                	'class': 'CancelButt',
			id: 'UCPANG-workflow-button',
	        	html: 'Workflow',
	        	title: 'open the workflow dialog',
                	events: {
                    		'click': function () {
		        		$('UCPANG-workflow-button').dispose();
	                		addWorkflowDialog(discussionTopic, ucpThread);
                    		}
                	}
            	}).inject($('GoodStuff').getElement('h2'), 'after');
	    	new Element('span', {
	        	html: '  '
            	}).inject($('GoodStuff').getElement('h2'), 'after');
		// prevent Steeev's inline editor to interfere :-)
		$('Main').addEventListener('DOMNodeInserted', newNodeHandler, true);
		$('Main').addEventListener('DOMNodeRemoved', removedNodeHandler, true);
		// click the workflow button when we know there is a workflow action needed
		switch (ucpThread.challengeStatus()) {
		case "Open":
			if (ucpThread.filled()) $('UCPANG-workflow-button').click();
			break;
		case "Filled":
			$('UCPANG-workflow-button').click();
			break;
		case "--VOTE--":
			if (ucpThread.finished()) $('UCPANG-workflow-button').click();
			break;
		case "Finished":
			if (!ucpThread.isClosed().closed ||
			    (ucpThread.isClosed().closed && ucpThread.isClosed().closer.innerHTML.match(GM_getLoggedInUser())))
				$('UCPANG-workflow-button').click();
			break;
		case "Closed":
			if (ucpThread.isClosed().closed && ucpThread.isClosed().closer.innerHTML.match(GM_getLoggedInUser())) $('UCPANG-workflow-button').click();
			break;
		}

	    }
	}
        var form = $$('table.TopicReply form textarea');
        if ($chk(form) && form.length > 0) {
            // add a 'bump this' on the page
            var bumpButton = new Element('button', {
                'class': 'Butt',
		id: 'UCPANG-bump-button',
                html: 'bump ',
                title: 'put this challenge thread at the top of the list',
                events: {
                    'click': function () {
                        bumpChallenge(ucpThread, this);
                    }
                }
            }).inject($('GoodStuff').getElement('h2'), 'after');
	    new Element('img', { src: images.bump }).inject(bumpButton);
        }
        processDiscussionTopic(discussionTopic, ucpThread, isAdminOrMod);
        ucpThread.store();

    } else if (pendingItemsPage && challengeGroup) {
        processPendingItems();
    } 

    ucpGroupConfigReader.checkForUpdates(groupConfig.groupname());

    return;

    // *******************
    //  End of processing
    // *******************
})();