Sunday, January 23, 2011

Unified CheckPlay for Flickr Challenge Groups


// ==UserScript==
// @name            UNIFIED CHALLENGES CheckPlay Tool (Flickr)
// @namespace       http://www.flickr.com/groups/
// @description     CheckPlay Tool for Flickr CHALLENGE groups
// @date            01/06/2008
// @creator         Kris Vangeel (http://flickr.com/kvgl69/) // original script for the Photo Face-Off challenge group
// @contributor     Andrew Dunn (http://flickr.com/andrewdunn/) // for changes in CYCheckPlay (Challenge You)
// @contributor     i_need_my_blankie (http://flickr.com/25084834@N03/) // for changes in FCCheckPlay (FRIENDLY Challenges)
// @contributor     Alesa Dam (http://flickr.com/alesadam/) // for making it generic, and adding features
// @modified        Jan. 20, 2011
// @version         1.1.1
//
// @include        http://www.flickr.com/groups/*/discuss*
// @exclude        http://www.flickr.com/groups/*/?search*
// @match          http://www.flickr.com/groups/*/discuss*
// @include        http://userscripts.org/*
// @match          http://userscripts.org/*
//
// @run-at         document-end
//
// ==/UserScript==
//
// ChangeLog: http://www.flickr.com/groups/unified_checkplay/discuss/72157623882744032/
// Documentation: http://www.flickr.com/groups/unified_checkplay/discuss/72157623600133810/
//
// --------------------------------------------------------------------
//
// 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.
//
// --------------------------------------------------------------------
// known issues:
// - greasemonkey does not work on FF3.6 and up on 64bit windows
// - in PIC-P-1, votes are 'lost' when a player changes his/her username
// - in PIC-P-1, there is no vote checking yet
// 
// desired features:
// - provide a panel with all the challenges I'm a player in, over the different groups
// - excludes may become invalid after a certain time
// - some groups require the last challenger to start the voting process
//      => type "mustvote" shows it
// - place a check box next to each photo, and create the vote for the user based on the chosen photo(s)
//      => dropped: photos without a link are ignored, voting that is in error difficult to anticipate, ..
// - incorporate functions for admins:
//      . mark error as checked
// - some challenges have predefined challengers (best of the best, MatchPoint, DUETOS)
//      . new state: instead of open/waiting, waitingForYou!
// - support for different photo sizes: icon challenges
// - add a 'check for medals' next to a player's entry
// - try to use some CPU power of the pipes.yahoo.com servers :)
//
//---------------------
// 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 us, and we will take care of it.
// We try to keep this as UNIFIED as possible.
// Contacts: 
// - Kate G. (http://www.flickr.com/photos/25084834@N03/)
// - Alesa Dam (http://www.flickr.com/photos/alesadam/)

// move the version check upfront
// in case something goes really wrong, changes are that a version bump could still be accessible
// don't place spaces around the '=' sign: incompatible with 'checkVersion' in older versions

(function() {
var CPtoolversion="V1.1.1";
var scriptNumber = 60303;

// 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));
  },

  trident: function(){
   return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4);
  },

  webkit: function(){
   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, */*'
  },
  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);
});

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

    var keyPrefix = 'UNIFIEDChallengesCheckPlayTool.';

    GM_getLoggedInUser = function () {
        try {
            return $('TopBar').getElement('table.Header').getElement('td.Status').getElement('a.Pale').get('html');
        } catch (e) {
            try {
                return $('TopBar').getElement('table.Header').getElement('td.Status').getElement('a.ywa-track').get('html');
            } 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 'a78ba83c374022595dc9073986735dcb'; // 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)) {
            return eval('(' + value + ')');
        }
        return null;
    }

    GM_storeObject = function (key, value) {
        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 'a78ba83c374022595dc9073986735dcb'; // the app's own key
    }

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

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

var updatingIcon = 'http://l.yimg.com/www.flickr.com/images/pulser2.gif';
var errorIcon = 'http://l.yimg.com/g/images/icon_error_x_small.png';
var defaultCheckIconSmall = 'http://l.yimg.com/g/images/icon_check_small.png';

function showUpdateNotification() {
    var color = 'white';
    var bgColor = 'black';
    var updatespan = new Element('span', {
        // 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 Challenges CheckPlay Tool: update available',
        href: (Browser.Engine.webkit || Browser.Engine.presto) ?
                'http://userscripts.org/scripts/show/' + scriptNumber: // Chrome users: first uninstall the old version
                'http://userscripts.org/scripts/source/' + scriptNumber + '.user.js',
        target: (Browser.Engine.webkit || Browser.Engine.presto) ? '_blank' : '',
        title:  (Browser.Engine.webkit || Browser.Engine.presto) ? 
            'to the script\'s install page (opens in new tab)' : 'click to install new version',
        styles: {
            'color': color,
            'text-decoration': 'none'
        },
        events: {
            click: function () {
                if (!(Browser.Engine.webkit || Browser.Engine.presto)) { // Firefox: install directly
                    this.innerHTML = 
                        "&#8595; &#8595; wait to reload the page until Greasemonkey finishes installing &#8595; &#8595;";
                    this.setStyle('color', "orange");
                    GM_deleteValue("onlineVersion"); 
                }
            }
        }
    }).inject(updatespan);
    new Element('a', {
        html: ' (Changes)',
        title: 'opens in new tab',
        href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623882744032/',
        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 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 run within this iframe => storeVersion is called
        GM_setValue("lastVersionCheckTime", CPStartTime.getTime().toString());
    }

    var onlineVersion = GM_getValue("onlineVersion");
    if ($chk(onlineVersion)) {
        var updateAvailable = false;
        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)) {
            updateAvailable = 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)) {
                updateAvailable = 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)) {
                    updateAvailable = true;
                }
            }
        }
        if (updateAvailable) {
            showUpdateNotification();
        }
    }
}

// -----------

var CPStartTime = new Date();

// former commonLibraryNG:
var UCPGroupConfigReader = new Class({
    timeBetweenReads: 7 * 24 * 60 * 60 * 1000, // a week
    groupListingURL: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623452533109/',
    initialize: function () {
    },
    checkForUpdates: function (groupname, force, callback) {
        var groupListCounter = GM_getValue("UCP.groupConfig.readCounter.list");
        if (!$chk(groupListCounter)) {
            groupListCounter = 0;
        }
        if (groupListCounter > 0) {
            GM_setValue("UCP.groupConfig.readCounter.list", groupListCounter - 1);
        } else {
            this.readGrouplistURL(false, callback);
        }
        if ($chk(GM_getValue("UCP.groupConfig." + groupname))) {
            var lastReadTime = GM_getValue("UCP.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);
            }
        }
    },
    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("UCP.groupConfig." + groupname))) {
            storedConfig = GM_getValue("UCP.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("UCP.groupConfig." + groupname));
            } catch (e) {
                // parse error?
                GM_deleteValue("UCP.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("UCP.groupConfig.list");
            this.readGroupConfigURL(groupname, true);
            storedConfig = GM_getValue("UCP.groupConfig." + groupname);
            if ($chk(storedConfig)) {
                try {
                    return new UCPGroupConfig(GM_getObject("UCP.groupConfig." + groupname));
                } catch (e) {
                    // parse error?
                    GM_deleteValue("UCP.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, callback);
        }
        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 announcement = tempDiv.getElement('td.Said p');
                announcement.getElements('small').each(function (small) { 
                    small.dispose(); 
                });
                var groupConfiguration = announcement.textContent
                                            .trim()
                                            .replace("&quot;", "\"")
                                            .replace(/\n/g, '') ; // Flickr changes
                //GM_log("groupConfiguration: " + groupConfiguration);
                var onlineConfig;
                try {
                    onlineConfig = JSON.parse(groupConfiguration);
                } catch (e) {
                    GM_log("json error: " + e); // JSON can't handle 'reName': /^d.../
                    // 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("UCP.groupConfig." + groupname, onlineConfig);
                // add a random factor to minimize concurrent read from UCP and UCPA
                // ony update after xx runs of checkForUpdates()
                GM_setValue("UCP.groupConfig.lastReadTime." + groupname, new Date().getTime().toString());
                if ($chk(callback)) {
                    callback( { stat: 'ok' } );
                }
            }.bind(this)
        }).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(); 
                });
                var groupList = announcement.textContent.trim().replace(/\n/g, '');
                var groups = {};
                try {
                    groups = JSON.parse(groupList);
                } catch(e) {
                    GM_log("json error: " + e);
                    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("UCP.groupConfig.list", groups);
                // update again after xx this.checkForUpdates() runs
                GM_setValue("UCP.groupConfig.readCounter.list", 37 + $random(0, 11));
                if ($chk(callback)) {
                    callback( { stat: 'ok' } );
                }
            }.bind(this)
        }).send();
    },
    groupList: function () {
        if ($chk(GM_getValue("UCP.groupConfig.list"))) {
            //GM_log("working with storage value, and update in background");
            try {
                return GM_getObject("UCP.groupConfig.list");
            } catch (e) { // parse error?
                GM_deleteValue("UCP.groupConfig.list");
                //return null; continue: reread
            }
        }
        //GM_log("reading list url");
        this.readGrouplistURL(true);
        //GM_log("returning stored dict: '" + ucpStoredGrouplist + "'");
        try {
            return GM_getObject("UCP.groupConfig.list");
        } catch (e) { // parse error?
            GM_deleteValue("UCP.groupConfig.list");
            return null;
        }
    }
});

var UCPGroupConfig = new Class({
    Implements: [Options],
    options: {
        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
    },
    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);
    },
    // accessors
    groupname: function () {
        return this.options.groupname;
    },
    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;
    },
    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;
    },
    checkPlayerEntries: function (args) {
        if (this.groupLimit() <= 0) {
            return;
        }
        var statusDiv;
        var statusParagraph;
        // TODO: for challenge groups where there is no group limit, but there is a limit for some specific challenges,
        // use the phrase "You have reached the maximum play limit in the capped competitions" (Fotocompetitions group)
        if (args.entries === this.groupLimit()) {
            statusDiv = $(args.statusDivId);
            statusDiv.style.display = 'block';
            statusParagraph = $(args.statusParagraphId);
            statusParagraph.set('html', "UCheckPlayNG: You entered " + args.entries + 
                " challenges and have reached your maximum play limit! " + 
                ($chk(this.groupLimitLabelAddon()) ? this.groupLimitLabelAddon() : ""));
            statusParagraph.style.color = 'red';
        }
        if (args.entries > this.groupLimit()) {
            statusDiv = $(args.statusDivId);
            statusDiv.style.display = 'block';
            statusParagraph = $(args.statusParagraphId);
            statusParagraph.set('html', "UCheckPlayNG: You entered over " + this.groupLimit() + 
                " challenges and are thus breaking the rules! Please remove your latest entry! " + 
                ($chk(this.groupLimitLabelAddon()) ? this.groupLimitLabelAddon() : ""));
            statusParagraph.style.color = 'red';
            statusParagraph.style.textDecorationUnderline = 'underline';
            statusParagraph.style.fontWeight = 'bold';
        }
    },
    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()) {
        //GM_log("name: " + name);
            if (this.challengeDefinitions().hasOwnProperty(name)) {
                var challengeDef = this.challengeDefinitions()[name];
                // TMOACG still uses reName (and others)
                var nameIndication = challengeDef.reName; // this 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(challengeName + " not found in definitions");
        return new UCPChallengeDefinition({
            scoreType: "UNKNOWN"
        });
    },
    skipChallenge: function (ucpThread) {
        if (ucpThread.challengeName().match(this.states().closed))  {
//        GM_log("challengeName: " + ucpThread.challengeName() + " matches closed state: " + this.states().closed);
            return true;
        }
        if (ucpThread.challengeName().match(this.states().expired)) {
            return true;
        }
        if (ucpThread.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 = challengeDef.reName;
                if (ucpThread.challengeName().match(specialName)) {
                    return false;
                }
            }
        }
        if (ucpThread.challengeName().match(this.states().open))    {
            return false;
        }
        if (ucpThread.challengeName().match(this.states().waitingForEntries)) {
            return false;
        }
        if (ucpThread.challengeName().match(this.states().vote))    {
            return false;
        }
        return true;
    },
    isGroupAdministratorOrModerator: function (userNsid) {
        var retval = GM_getValue("UCP.groupAdminOrMod." + this.groupname() + "." + userNsid) === true ||
                     GM_getValue("UCP.groupAdminOrMod." + this.groupname() + "." + userNsid) === 'true';
        var lastReadTime = GM_getValue("UCP.groupAdminOrMod." + this.groupname() + "." + userNsid + ".lastReadTime");
        //GM_log("admin: " + retval + " - time: " + lastReadTime);
        var asyncUpdate = true;
        if (!$chk(lastReadTime)) {
            //GM_log("using API to retrieve admin info");
            asyncUpdate = false;
            retval = false;
        } else {
            // run this test only once a day
            var now = new Date().getTime();
            var elapsedTime = now - lastReadTime;
            if (elapsedTime < 24 * 60 * 60 * 1000) {
                return retval;
            }
        }
        var groupname = this.groupname();
        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: this.groupId(),
            membertypes: '3,4',
            per_page: 500 // one page should be sufficient?
        };
        new Request({
            url: "http://api.flickr.com/",
            async: asyncUpdate,
            onFailure: function (response) {
                retval = false;
                GM_log("failed reading members from group");
            },
            onSuccess: function (responseText, responseXML) {
                var retval = 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);
                    retval = false;
                    return;
                }
                var members = result.members;
                $each(members.member, function (member) {
                    if (member.nsid === userNsid) {
                        retval = true;
                    }
                });
                GM_setValue("UCP.groupAdminOrMod." + groupname + "." + userNsid, retval);
                GM_setValue("UCP.groupAdminOrMod." + groupname + "." + userNsid + ".lastReadTime", 
                            new Date().getTime().toString());
            }
        }).get("/services/rest", apiData);

        // debug: me
        if (userNsid === '37989307@N08') {
            return 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 ucpLanguages = [];

// TODO: remove playerMayVote, playerFinished, playerMustVote, playerVoted titles from languages
var UCPLanguageConfigReader = new Class({
    timeBetweenReads: 30 * 24 * 60 * 60 * 1000, // a month
    languageListingURL: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623513185619/',
    initialize: function () {
    },
    checkForUpdates: function (languagename, force, callback) {
        if ($chk(GM_getValue("UCP.languageConfig." + languagename))) {
            //GM_log("found " + languagename + " in storage, updating in background");
            // update in the background
            // make sure the language list is read also: a user should not have to hit F5 twice, just to get support!
            var lastReadTime = GM_getValue("UCP.languageConfig.lastReadTime." + languagename);
            var now = new Date().getTime();
            var elapsedTime = $chk(lastReadTime) ? now - lastReadTime : this.timeBetweenReads + 1;
            if (elapsedTime > this.timeBetweenReads || force) {
                GM_log("updating '" + languagename + "' definitions");
                this.readLanguageConfigURL(languagename, force === true ? true : false, callback);
            }
        }
    },
    createLanguage: function (languagename, legacyLabels, languageOverrides) {
        if (!$chk(languagename)) {
            languagename = 'English';
        }
        if ($chk(ucpLanguages[languagename])) {
            return ucpLanguages[languagename];
        }
        //GM_log("reading config for '" + languagename + "'");
        var storedConfig;
        if ($chk(GM_getValue("UCP.languageConfig." + languagename))) {
            storedConfig = GM_getValue("UCP.languageConfig." + languagename);
        }
        if (!$chk(storedConfig)) {
        //GM_log(languagename + " not found in storage, updating");
            this.readLanguageConfigURL(languagename, true);
            storedConfig = GM_getValue("UCP.languageConfig." + languagename);
        }
        // use the stored config, and read updates, in the background
        var result;
        try {
            result = JSON.parse(storedConfig);
        } catch (e) {
            result = eval('(' + storedConfig + ')');
        }
        if ($chk(result)) {
        //GM_log("storing language " + languagename);
            GM_storeObject("UCP.languageConfig." + languagename, result);
            // inject languagename
            result.language = languagename;
            var retval = new UCPLanguage(result);
            if ($chk(languageOverrides)) {
                retval.useLanguageOverrides(languageOverrides);
            }
            if ($chk(legacyLabels)) {
                retval.useLegacyLabels(legacyLabels);
            }
            ucpLanguages[languagename] = retval;
            return retval;
        } else {
            GM_log("result is invalid (storedConfig: " + storedConfig + ")");
            GM_deleteValue("UCP.languageConfig." + languagename);
            // retry with English
            return this.createLanguage('English', legacyLabels, languageOverrides);
        }
        return null;
    },
    readLanguageConfigURL: function (languagename, synchronous, callback) {
        //GM_log("reading language url for '" + languagename + "'");
        try {
            var languageList = GM_getObject("UCP.languageConfig.list");
        } catch (e) { // parse error
            GM_deleteValue("UCP.languageConfig.list");
        }
        if (!$chk(languageList)) {
            this.readLanguagelistURL(true, callback);
            languageList = GM_getObject("UCP.languageConfig.list");
        }
        var languageUrl = languageList[languagename].definitions;
        //GM_log("reading language definitions for '" + languagename + "': '" + languageUrl + "'");
        var request = new Request({
            method: 'get',
            url: languageUrl,
            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(); 
                });
                var languageConfiguration = announcement.textContent
                                            .trim()
                                            .replace("&quot;", "\"")
                                            .replace(/\n/g, '');
                //GM_log("languageConfiguration: " + languageConfiguration);
                var onlineConfig;
                try {
                    onlineConfig = JSON.parse(languageConfiguration);
                } catch (e) {
                    GM_log("json error: " + e);
                    try {
                        onlineConfig = eval ('(' + languageConfiguration + ')');
                    } catch (e) {
                        GM_log("error reading languageConfiguration: " + e);
                        GM_log("language configuration: " + languageConfiguration);
                        if ($chk(callback)) {
                            callback( { stat: 'error', error: 'error reading languageConfiguration: ' + e } );
                        }
                        return;
                    }
                }
                if (languagename !== 'English') {
                    // reset defaults for non-defined parts, or languages
                    // TODO: document
                    var defaultLanguage = this.createLanguage('English');
                    this.checkForUpdates('English');
                    if (!$chk(onlineConfig.titles)) {
                        onlineConfig.titles = defaultLanguage.titles();
                    } else {
                        for (var title in defaultLanguage.titles()) {
                            if (defaultLanguage.titles().hasOwnProperty(title)) {
                                if (!$chk(onlineConfig.titles[title])) {
                                //GM_log("title '" + title + "' not found, replacing with default");
                                    onlineConfig.titles[title] = defaultLanguage.titles()[title];
                                }
                            }
                        }
                    }
                    if (!$chk(onlineConfig.labels)) {
                        onlineConfig.labels = defaultLanguage.labels();
                    } else {
                        for (var label in defaultLanguage.labels()) {
                            if (defaultLanguage.labels().hasOwnProperty(label)) {
                                if (!$chk(onlineConfig.labels[label])) {
                                //GM_log("label '" + label + "' not found, replacing with default (" + defaultLanguage.labels()[label] + ")");
                                    onlineConfig.labels[label] = defaultLanguage.labels()[label];
                                }
                            }
                        }
                    }
                }
                GM_storeObject("UCP.languageConfig." + languagename, onlineConfig);
                // only update after xx runs of checkForUpdates()
                GM_setValue("UCP.languageConfig.lastReadTime." + languagename, new Date().getTime().toString()); 
                if ($chk(callback)) {
                    callback( { stat: 'ok' } );
                }
            }.bind(this)
        }).send();
    },
    readLanguagelistURL: function (synchronous, callback) {
        //GM_log("reading language list");
        var request = new Request({
            method: 'get', 
            url: this.languageListingURL,
            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(); 
                });
                var languageList = announcement.textContent;
                this.languages = {};
                //GM_log("languageList: " + languageList);
                try {
                    this.languages = JSON.parse(languageList);
                } catch (e) {
                    try {
                        this.languages = eval('(' + languageList + ')');
                    } catch (e) {
                        GM_log("error parsing languageList result: " + e);
                        GM_log("languageList: " + languageList);
                        if ($chk(callback)) {
                            callback( { stat: 'error', error: 'error parsing languagelist result: ' + e } );
                        }
                        return;
                    }
                }
                GM_storeObject("UCP.languageConfig.list", this.languages);
                if ($chk(callback)) {
                    callback( { stat: 'ok' } );
                }
            }.bind(this)
        }).send();
    },
    getLanguageList: function () {
        try {
            var list = GM_getObject("UCP.languageConfig.list");
        } catch (e) { // parse error
            GM_deleteValue("UCP.languageConfig.list");
        }
        if (list === undefined) {
            this.readLanguagelistURL(true);
            list = GM_getObject("UCP.languageConfig.list");
        }
        return list;
    }
});

var UCPLanguage = new Class({
    Implements: Options,
    options: {
        name: undefined,
        titles: undefined,
        labels: undefined
    },
    initialize: function (options) {
        this.setOptions(options);
    },
    name: function () {
        return this.options.name;
    },
    titles: function () {
        return this.options.titles;
    },
    labels: function () {
        return this.options.labels;
    },
    useLegacyLabels: function (legacyLabels) {
        for (var label in legacyLabels) {
            if (legacyLabels.hasOwnProperty(label)) {
                this.options.labels[label] = legacyLabels[label];
            }
        }
    },
    useLanguageOverrides: function (languageOverrides) {
        if ($chk(languageOverrides)) {
            if ($chk(languageOverrides.titleOverrides)) {
                var titleOverrides = languageOverrides.titleOverrides;
                for (var titleOverride in titleOverrides) {
                    if (titleOverrides.hasOwnProperty(titleOverride)) {
                        this.options.titles[titleOverride] = this.options.titles[titleOverrides[titleOverride]];
                    }
                }
            }
            if ($chk(languageOverrides.labelOverrides)) {
                var labelOverrides = languageOverrides.labelOverrides;
                for (var labelOverride in labelOverrides) {
                    if (labelOverrides.hasOwnProperty(labelOverride)) {
                        this.options.labels[labelOverride] = this.options.labels[labelOverrides[labelOverride]];
                    }
                }
            }
        }
    }
});

function createTopicListingStatusDecorator (chlgstatus, ucpGroupPreferences, ucpLanguage) {
        if (!$chk(ucpLanguage)) {
            ucpLanguage = new UCPLanguageConfigReader().createLanguage(ucpGroupPreferences.language(),
                ucpGroupPreferences.useLegacyLabels() && ucpGroupPreferences.groupConfig().hasLegacyLabels() ?
                    ucpGroupPreferences.groupConfig().legacyLabels() : undefined,
                ucpGroupPreferences.groupConfig().languageOverrides());
        }
        if (!$chk(ucpLanguage)) {
            ucpLanguage = new UCPLanguageConfigReader().createLanguage('English',
                ucpGroupPreferences.useLegacyLabels() && ucpGroupPreferences.groupConfig().hasLegacyLabels() ?
                    ucpGroupPreferences.groupConfig().legacyLabels() : undefined,
                ucpGroupPreferences.groupConfig().languageOverrides());
        }
        
        switch (chlgstatus) {
        case "Filled":
            return {
                name: chlgstatus,
                // TODO: use scoreTypeLabelOverride from challengeDefinition
                labels: [ ucpLanguage.labels().filled, 
                          ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.labels().vote : ""
                        ], 
                title: ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : ""),
                color: ucpLanguage.labels().filledColor,
                addWarning: true
            };
        case "Finished":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().finished ],
                title: ucpLanguage.titles().finished,
                color: ucpLanguage.labels().finishedColor,
                addWarning: true
            };
        case "--VOTE--": // fall through
        case "VOTE":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().vote ],
                title: ucpLanguage.titles().vote,
                color: ucpLanguage.labels().voteColor,
                addWarning: true
            };
        case "Voted":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().voted ],
                title: ucpLanguage.titles().voted,
                color: ucpLanguage.labels().votedColor,
                addWarning: true
            };
        case "Voided": 
        case "Closed":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().closed ],
                title: ucpLanguage.titles().closed,
                color: ucpLanguage.labels().ignoreColor
            };
        case "Open":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().open ],
                title: ucpLanguage.titles().open,
                color: ucpLanguage.labels().openColor,
                addWarning: true
            };
        case "OK":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().waitingForEntries ],
                title: ucpLanguage.titles().open,
                color: ucpLanguage.labels().waitingForEntriesColor,
                addWarning: true
            };
        case "Excluded":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().excluded ],
                title: ucpLanguage.titles().excluded,
                color: ucpLanguage.labels().excludedColor,
                addWarning: true
            };
        case "ErrExclPlay":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().errExcl ],
                title: ucpLanguage.titles().errExclPlay,
                color: ucpLanguage.labels().errExclColor,
                addWarning: true
            };
        case "UPDATING":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().updating ],
                title: ucpLanguage.titles().updating,
                color: ""
            };
        case "ERRORLOADING":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().errorLoading ],
                title: ucpLanguage.titles().errorLoading,
                color: ucpLanguage.labels().errorLoadingColor,
                addWarning: true
            };
        case "ERRORPARSING":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().errorParsing ],
                title: ucpLanguage.titles().errorParsing,
                color: ucpLanguage.labels().errorParsingColor,
                addWarning: true
            };
        case "Player":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player ],
                title: ucpLanguage.titles().player,
                color: ucpLanguage.labels().playerColor,
                addWarning: true
            };
        case "PlayerVoted":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().voted ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().voted,
                color: ucpLanguage.labels().votedColor,
                addWarning: true
            };
        case "PlayerMustVote":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().vote ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().vote,
                color: ucpLanguage.labels().voteColor,
                addWarning: true
            };
        case "PlayerMayVote":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player ],
                title: ucpLanguage.titles().player,
                color: ucpLanguage.labels().playerColor,
                addWarning: true
            };
        case "PlayerFilled":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().filled ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : ""),
                color: ucpLanguage.labels().filledColor,
                addWarning: true
            };
        case "PlayerFinished":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().player, ucpLanguage.labels().finished ],
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().finished,
                color: ucpLanguage.labels().finishedColor,
                addWarning: true
            };
        case "UNKNOWN":
        case "Unknown":
        case "---":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().ignore ],
                title: ucpLanguage.titles().ignore,
                color: ""
            };
        case "none":
            return {
                name: chlgstatus,
                labels: [ ucpLanguage.labels().open + "?" ],
                title: ucpLanguage.titles().open,
                color: ucpLanguage.labels().openColor,
                addWarning: true
            };
        default:
            GM_log("no decorator for chlgstatus '" + chlgstatus + "'");
            return { name: "error" };
        }
}

function createChallengePageStatusDecorator (chlgstatus, ucpGroupPreferences, ucpLanguage) {
        if (!$chk(ucpLanguage)) {
            ucpLanguage = new UCPLanguageConfigReader().createLanguage(ucpGroupPreferences.language(),
                ucpGroupPreferences.useLegacyLabels() && ucpGroupPreferences.groupConfig().hasLegacyLabels() ?
                    ucpGroupPreferences.groupConfig().legacyLabels() : undefined,
                ucpGroupPreferences.groupConfig().languageOverrides());
        }
        if (!$chk(ucpLanguage)) {
            ucpLanguage = new UCPLanguageConfigReader().createLanguage('English',
            ucpGroupPreferences.useLegacyLabels() && ucpGroupPreferences.groupConfig().hasLegacyLabels() ?
                ucpGroupPreferences.groupConfig().legacyLabels() : undefined,
            ucpGroupPreferences.groupConfig().languageOverrides());
        }
        switch (chlgstatus) {
        case "Filled":
            return { 
                name: chlgstatus,
                title: ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : "")
            };
        case "Finished":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().finished
            };
        case "--VOTE--": // fall through
        case "VOTE":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().vote
            };
        case "Voted":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().voted
            };
        case "Voided": 
        case "Closed":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().closed
            };
        case "Open":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().open
            };
        case "OK":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().open
            };
        case "Excluded":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles()
            };
        case "ErrExclPlay":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().errExclPlay
            };
        case "UPDATING":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().updating
            };
        case "ERRORLOADING":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().errorLoading
            };
        case "ERRORPARSING":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().errorParsing
            };
        case "Player":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player
            };
        case "PlayerVoted":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().voted
            };
        case "PlayerMustVote":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().vote
            };
        case "PlayerMayVote":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player
            };
        case "PlayerFilled":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().filled + 
                    (ucpGroupPreferences.groupConfig().automaticVoteStart() ? ucpLanguage.titles().automaticVoteStart : "")
            };
        case "PlayerFinished":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().player + " " + ucpLanguage.titles().finished
            };
        case "---":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().ignore
            };
        case "none":
            return {
                name: chlgstatus,
                title: ucpLanguage.titles().open
            };
        default:
            GM_log("don't know what to do with status '" + chlgstatus + "'");
            return { name: "error" };
        }
}

var UCPChallengeDefinition = new Class({
    Implements: [Options],
    options: {
        name: undefined,
        reName: /.*/,
        neededPhotos: -1,
        neededScore: -1,
        scoreType: undefined,
        countsToLimit: false,
        limitPerPlayer: -1,
        playerVoting: 'mayvote',
        scoresAdded: true,
        iconChallenge: false,
        scoreTypeLabelOverride: undefined
    },
    initialize: function (options) {
        this.setOptions(options);
    },
    name: function () {
        return this.options.name;
    },
    reName: function () {
        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;
    },
    scoreTypeLabelOverride: function () {
        return this.options.scoreTypeLabelOverride;
    },
    setScoreTypeLabelOverride: function (s) {
        this.options.scoreTypeLabelOverride = 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;
    },
    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;
    },
    readChallengeDefinitionOverrides: function (announcementNode) {
        var photos, votes, voteType, maxPhotos, playerVoting, countsToLimit, scoresAdded, iconChallenge;
        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 scoreTypeLabelOverrideMatch = override.match(/:scoretypelabel:(\w+)$/);
                if (scoreTypeLabelOverrideMatch) {
                    var scoreTypeLabelOverride = scoreTypeLabelOverrideMatch[1];
                    if (this.scoreTypeLabelOverride() !== scoreTypeLabelOverride) {
                        overridesDefined = true;
                        this.setScoreTypeLabelOverride(scoreTypeLabelOverride);
                    }
                }
                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);
                    }
                }
            }, this);
        }
        return overridesDefined;
    }
});

var ucpInsertionIdx = 0;

function ucpCreateChallengeThread(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,
        chlgAnchor: undefined,
        labelElement: undefined,
        feedbackElement: undefined,
        scoreAnchor: undefined,
        chlgstatus: "UPDATING",
        challengeDefinition: undefined,
        replies: 0,
        votingErrors: [],
        validVotingAsStored: true,
        excludedPlayers: [],
        url: undefined,
        topic: undefined,
        scoreSummary: undefined,
        lastLoadTime: undefined,
        needsStatusOnTopicListing: false,
        needsStatusOnChallengePage: false
    },
    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(),
                  "labelElement: " + this.labelElement(),
                  "chlgstatus:   " + this.challengeStatus(),
                  "validVoting:  " + this.validVoting(),
                  "votingErrors:  " + this.votingError(),
                  "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;
    },
    challengeAnchor: function () {
        return this.options.chlgAnchor;
    },
    labelElement: function () {
        return this.options.labelElement;
    },
    setLabelElement: function (element) {
        this.options.labelElement = element;
    },
    scoreAnchor: function () {
        return this.options.scoreAnchor;
    },
    setScoreAnchor: function (element) {
        this.options.scoreAnchor = element;
    },
    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;
    },
    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);
        }
    },
    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;
    },
    lastLoadTime: function () {
        return this.options.lastLoadTime;
    },
    setLastLoadTime: function (time) {
        this.options.lastLoadTime = time;
    },
    store: function () {
        GM_storeObject("UCP." + this.groupname() + "." + this.topic(), {
                chlgname:       this.challengeName(),
                chlgstatus:     this.challengeStatus(),
                scoreSummary:   (this.scoreSummary() ? this.scoreSummary() : ""),
                replies:        this.replies(),
                votingError:    !this.validVoting(),
                lastLoadTime:   (this.lastLoadTime() ? this.lastLoadTime() : 0)
            });
    },
    retrieve: function () {
        var value = GM_getObject("UCP." + this.groupname() + "." + this.topic());
        if (value) {
            this.options.chlgname = value.chlgname;
            this.setChallengeStatus(value.chlgstatus);
            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") {
//        GM_log("updating status to closed");
            this.setChallengeStatus("Closed");
            return;
        }
        if (value === "voided") {
            this.setChallengeStatus("Voided");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "open")) {
            this.setChallengeStatus("Open");
            return;
        }
        // set for photoposter (first for loop)
        if (value === "Excluded") {
            this.setChallengeStatus("Excluded");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "photoposter")) {
            this.setChallengeStatus("Player");
            return;
        }
        if ((this.challengeStatus() === "Excluded") && (value === "photoposter")) {
            this.setChallengeStatus("ErrExclPlay");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "filled")) {
            this.setChallengeStatus("Filled");
            return;
        }
        // set for voter (second for loop)
        if ((this.challengeStatus() === "none") && (value === "voter")) {
            this.setChallengeStatus("Voted");
            return;
        }
        if ((this.challengeStatus() === "Excluded") && (value === "voter")) {
            this.setChallengeStatus("Voted");
            return;
        }
        if ((this.challengeStatus() === "ErrExclPlay") && (value === "voter")) {
            this.setChallengeStatus("ErrExclPlay");
            return;
        }
        if ((this.challengeStatus() === "Player") && (value === "voter")) {
            this.setChallengeStatus("PlayerVoted");
            return;
        }
        if ((this.challengeStatus() === "Voted") && (value === "voter")) {
            this.setChallengeStatus("Voted"); //catch a comment and a vote from same player
            return;
        }
        if ((this.challengeStatus() === "Voted") && (value === "photoposter")) {
            this.setChallengeStatus("PlayerVoted");
            return;
        }
        if ((this.challengeStatus() === "Player") && (value === "mayvote")) {
            this.setChallengeStatus("PlayerMayVote");
            return;
        }
        if ((this.challengeStatus() === "Player") && (value === "mustvote")) {
            this.setChallengeStatus("PlayerMustVote");
            return;
        }
        if (this.challengeStatus().match(/^Player/) && (value === "voter")) {
            this.setChallengeStatus("PlayerVoted");
            return;
        }
        if (this.challengeStatus().match(/^Player/) && (value === "finished")) {
            this.setChallengeStatus("PlayerFinished");
            return;
        }
        if (this.challengeStatus().match("Finished") && (value === "photoposter")) {
            this.setChallengeStatus("PlayerFinished");
            return;
        }
        if (this.challengeStatus() === "Player") {
            this.setChallengeStatus("Player"); // don't overwrite in case of 'maynotvote'
            return;
        }
        if (value === "finished") {
            this.setChallengeStatus("Finished");
            return;
        }
        if ((this.challengeStatus() === "none") && (value === "Unknown")) {
            this.setChallengeStatus("Unknown");
            return;
        }
        // this.setChallengeStatus(""); keep the status as it is! => [Finished] => RE-VOTE => voter => error!
    },
    printStatus: function (ucpGroupPreferences, ucpLanguage, newchlgstatus) {
        if (!newchlgstatus || newchlgstatus.length === 0) {
            newchlgstatus = this.challengeStatus();
        }
        var statusData;
        if (this.options.needsStatusOnTopicListing) {
            statusData = createTopicListingStatusDecorator(newchlgstatus, ucpGroupPreferences, ucpLanguage);
        } else if (this.options.needsStatusOnChallengePage) {
            statusData = createChallengePageStatusDecorator(newchlgstatus, ucpGroupPreferences, ucpLanguage);
        } else {
            return;
        }
        this.decorateThread(statusData);
        // TODO: part of UCPListing.printStatus
        this.groupConfig().checkPlayerEntries({
                entries: playernumber,
                statusDivId: "UCheckPlayNGStatusDiv",
                statusParagraphId: "UCheckPlayNGStatus"
        });
    },
    decorateThread: function (statusData) {
        if (this.options.needsStatusOnTopicListing) {
            if ($chk(this.scoreAnchor()) && $chk(this.scoreSummary()) && 
                (statusData.name === "Finished"    || statusData.name === "Voted" || 
                 statusData.name === "PlayerVoted" || statusData.name === "PlayerFinished" ||
                 statusData.name === "Closed")) {
                this.scoreAnchor().title = "UCheckPlay score summary: " + this.scoreSummary();
            }

            if (this.labelElement()) {
                this.labelElement().empty();
                this.labelElement().style.textDecoration = 'none';
                var myColor = statusData.color;
                var span;
                statusData.labels.each(function (label) {
                    this.labelElement().adopt(span = new Element('span', {
                        html: label,
                        styles: {
                            color: myColor
                        },
                        title: statusData.title
                    }));
/*                if (label.match(/^<img/)) {
                GM_log("using image " + label);
                    span.innerHTML = label;
                } */
                }, this);
                if (statusData.addWarning && !this.validVoting()) {
                    span.set('html', span.get('html') + '(!)');
                    span.set('title', span.get('title') + ' (!! ' + this.votingError() + ')');
                }
            }
            if (this.challengeAnchor()) {
                this.challengeAnchor().set('title', this.options.title);
            }
        } else if (this.options.needsStatusOnChallengePage) {
            var showSummary = $chk(this.scoreAnchor()) && $chk(this.scoreSummary()) && 
                (statusData.name === "Finished"    || statusData.name === "Voted" || 
                 statusData.name === "PlayerVoted" || statusData.name === "PlayerFinished" ||
                 statusData.name === "Closed");
            if (showSummary) {
                this.scoreAnchor().title = "UCheckPlayNG score summary: " + this.scoreSummary();
            }

            if (this.feedbackElement()) {
                this.feedbackElement().empty();
                if (statusData.title) {
                    this.feedbackElement().set('html', 'UCheckPlayNG: ' + statusData.title + '<br/>');
                }
                if (showSummary) {
                    new Element('small', {
                        html: "UCheckPlayNG: " + this.scoreSummary() + "<br/>"
                    }).inject(this.feedbackElement(), 'after');
                }
            }
        }
    },
    printExcludes: function (challengeAnnouncement) {
        if (!this.options.needsStatusOnChallengePage) {
            return;
        }
        try {
            if ($chk(this.options.excludedPlayers) && this.options.excludedPlayers.length > 0) {
                var pageAnchor = challengeAnnouncement.getElements('small').getLast();
                if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
                    pageAnchor = pageAnchor.getParent('div.ucpdiv');
                }
                if (!$chk(pageAnchor)) {
                    pageAnchor = challengeAnnouncement;
                }
                this.excludedPlayers().each(function (excludedPlayer) {
                        new Element('small', {
                            html: "UCheckPlayNG: found exclude for <b>" + excludedPlayer + "</b><br/>"
                        }
                    ).inject(pageAnchor, 'after');
                });
            }
        } catch (e) {
            GM_log("error: " + e);
        }
    },
    sort: function (groupPreferences, topicListingTable) {
        if (!$chk(this.labelElement())) {
            return;
        }
        //ucpInsertionIdx; // TODO: move to UCPTopicListingTable to make it work again as before
        if (groupPreferences.ucpSort()) {
            // move VOTE threads to the top, closed and voted threads to the bottom
            var moveUp = undefined, moveDown = undefined;
            var newchlgstatus = this.challengeStatus();
            moveUp =   (newchlgstatus === "VOTE"           || 
                        newchlgstatus === "--VOTE--"       || 
                        newchlgstatus === "PlayerMustVote");
            moveDown = (newchlgstatus === "---"            || 
                        newchlgstatus === "Excluded"       || 
                        newchlgstatus === "Voted"          || 
                        newchlgstatus === "Closed"         ||
                        newchlgstatus === "Finished"       ||
                        newchlgstatus === "PlayerFinished" ||
                        newchlgstatus === "PlayerVoted");
            if (moveUp && ucpInsertionIdx === 0 && // if we already have moved a row, it should be fine
                $$('a[name^=infinitepage]').length > 0) {
                var autopageWarning = $('UCheckPlayAutoPageWarning');
                if (!autopageWarning) { // only show it once!
                    new Element('p', {
                        id: "UCheckPlayAutoPageWarning",
                        html: "UCheckPlay: moving rows up when AutoPage is active may create chaos! " +
                              "Skipping.<br/>" +
                              "You may consider turning of the AutoPage script for this page, or " +
                              "not using the UCPstyle for this group",
                        styles: {
                            textDecoration: 'none',
                            color: 'brown',
                            display: 'block'
                        }
                    }).inject('UCheckPlayStatusDiv');
                    $('UCheckPlayStatusDiv').setStyle('display', 'block');
                }
            } else if (moveUp || moveDown) {
                if (topicListingTable) {
                    var tableRows = topicListingTable.getElements('tr');
                    if (tableRows && tableRows.length > 0) {
                        var row = this.labelElement().getParent('tr');
                        if (row) {
                            if (moveUp) {
                                var previousRow = tableRows[ucpInsertionIdx++]; // when 0, inserts after header
                                $(row).inject(previousRow, 'after');
                            } else if (moveDown) {
                                // don't move the last row down, after itself!
                                // to prevent this from happening, first add a dummy row, and remove it afterwards
                                if (row !== topicListingTable.getLast()) {
                                    $(row).inject(topicListingTable.getLast(), 'after');
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    checkStatus: function (photosposted, debug) {
    if (debug) GM_log("checkStatus: chlgstatus = '" + this.challengeStatus() + "', waiting state: '" + this.groupConfig().states().waitingForEntries + "', challenge name: '" + this.challengeName());
        if (this.challengeName().match(this.groupConfig().states().closed)) {
//        GM_log("name: " + this.challengeName() + " matches " + this.groupConfig().states().closed);
            this.updateStatus("closed");
        }
        if (this.challengeName().match(this.groupConfig().states().voided)) {
            this.updateStatus("voided");
        }
        if ((this.challengeStatus() === "none") && this.filled(photosposted)) {
            this.updateStatus("filled");
        }
        if ((this.challengeStatus() === "none") && this.open(photosposted)) {
            this.setChallengeStatus("Open");
        } 
        if ((this.challengeStatus() === "none") && this.waitingForEntries(photosposted)) {
            this.setChallengeStatus("OK");
        } 
        if (  (this.challengeStatus() === "none" || this.challengeStatus() === "Excluded") 
            && $chk(this.challengeName().match(this.groupConfig().states().vote))) {
            if (debug) GM_log("setting to --VOTE--: " + this.challengeName() + " matches " + this.groupConfig().states().vote);
            this.setChallengeStatus("--VOTE--");
        }
        if ((this.challengeStatus() === "Player") && 
            (this.filled(photosposted) || this.challengeName().match(this.groupConfig().states().vote))) {
            this.updateStatus(this.challengeDefinition().playerVoting());
        }
        if (this.challengeStatus() === "PlayerMayVote" || this.challengeStatus() === "Player") {
            if (this.filled(photosposted)) {
                this.setChallengeStatus("PlayerFilled");
            }
            return;
        }
    },
    resetStatus: function () {
        this.setChallengeStatus("none");
        this.votingErrors = [];
    },
    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 threadNr = this.challengeName().match(/^\d+/);
        if ($chk(threadNr)) {
            this.startload = new Date();
        }*/
        var ucpThread = this;
        new Request({
            url: ucpThread.url(),
            async: true,
            headers: {
                //"User-Agent": "Mozilla/5.0",            // If not specified, navigator.userAgent will be used.
                "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());
                ucpThread.resetStatus();
                ucpThread.setChallengeStatus("ERRORLOADING");
                ucpThread.store();
                ucpThread.printStatus(groupPreferences, ucpLanguage);
            },
            onSuccess: function (responseText, responseXML) {
                /*if ($chk(threadNr)) {
                    ucpThread.pageloaded = new Date();
                }*/
//                GM_log("headers: " + this.xhr.headers);
                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(discussionTopic, 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);
                ucpThread.setChallengeStatus("ERRORLOADING");
                processDiscussionTopicCallback(discussionTopic, ucpThread);
                ucpThread.store();
                ucpThread.printStatus(groupPreferences, ucpLanguage);
            },
            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) {
        return ucpCollectVotes(this, 
                    this.groupConfig().allowsPhotoInAnnouncement() ? challengeAnnouncement : null,
                    challengeEntries
                    );
    }
});

var UCPNonChallengeThread = new Class({
    Extends: UCPThread,
    initialize: function (options) {
        this.parent(options);
    },
    printStatus: function (groupPreferences, ucpLanguage, newchlgstatus) {
        if (!$chk(ucpLanguage)) {
            ucpLanguage = new UCPLanguageConfigReader().createLanguage(groupPreferences.language(),
                groupPreferences.useLegacyLabels() && groupPreferences.groupConfig().hasLegacyLabels() ?
                    groupPreferences.groupConfig().legacyLabels() : undefined,
                groupPreferences.groupConfig().languageOverrides());
        }
        if (!$chk(ucpLanguage)) {
            ucpLanguage = new UCPLanguageConfigReader().createLanguage('English',
                groupPreferences.useLegacyLabels() && groupPreferences.groupConfig().hasLegacyLabels() ?
                    groupPreferences.groupConfig().legacyLabels() : undefined,
                groupPreferences.groupConfig().languageOverrides());
        }
        var anchortitle = ucpLanguage.titles()[this.getLabelPrefix()];
        if (this.labelElement()) {
            this.labelElement().set('html', ucpLanguage.labels()[this.getLabelPrefix()]);
            this.labelElement().set ('style', 
                'color: ' + ucpLanguage.labels()[this.getLabelPrefix() + "Color"] + '; text-decoration: none');
            this.labelElement().title = anchortitle;
            this.challengeAnchor().title = anchortitle;
        }
    },
    getLabelPrefix: function () {},

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

// TODO: use new UCPNonChallengeThread({status: 'Chat', labelPrefix: 'chat'});
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 (photosposted) {
        if (this.challengeDefinition().neededPhotos() <= 0) {
            return false;
        }
        if (photosposted >= this.challengeDefinition().neededPhotos() &&
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        return false;
    },
    open: function (photosposted) {
        //GM_log("checking name against open states: " + this.chlgname + " - " + states.open + " - " + states.waitingForEntries);
        if (photosposted === 0 && 
                (this.challengeName().match(this.groupConfig().states().open) || 
                 this.challengeName().match(this.groupConfig().states().waitingForEntries))) {
            return true;
        }
        return false;
    },
    waitingForEntries: function (photosposted) {
        if (!$chk(photosposted)) {
            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 (photosposted < this.challengeDefinition().neededPhotos()
                && (this.challengeName().match(this.groupConfig().states().waitingForEntries) ||
                    this.challengeName().match(this.groupConfig().states().open)));
    },
    finished: function(vote) {
        // 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) {
            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();
    }
});

UCPStylePreferences = new Class({
    Implements: [Options],
    options: {
        removeNew: true,
        firstColumn: true,
        sort: true
    },
    initialize: function (group, options) {
        this.setOptions(options);
        if (!options) {
            var storedRemoveNew = GM_getValue("UCP.ucpStyle.removeNew." + group.groupname());
            this.options.removeNew = (storedRemoveNew === true || storedRemoveNew === 'true');
            var storedFirstColumn = GM_getValue("UCP.ucpStyle.firstColumn." + group.groupname());
            this.options.firstColumn = (storedFirstColumn === true || storedFirstColumn === 'true');
            var storedSort = GM_getValue("UCP.ucpStyle.sort." + group.groupname());
            this.options.sort = (storedSort === true || storedSort === 'true');
        }            
    },
    equals: function (other) {
        if (!other) {
            return false;
        }
        return this.options.removeNew === other.options.removeNew &&
                this.options.firstColumn === other.options.firstColumn &&
                this.options.sort === other.options.sort;
    },
    store: function (group) {
        var ucpStyleRemoveNew = GM_setValue("UCP.ucpStyle.removeNew." + group.groupname(), this.options.removeNew);
        var ucpStyleFirstColumn = GM_setValue("UCP.ucpStyle.firstColumn." + group.groupname(), this.options.firstColumn);
        var ucpStyleSort = GM_setValue("UCP.ucpStyle.sort." + group.groupname(), this.options.sort);
    }    
});

UCPGroupPreferences = new Class({
    Implements: [Options],
    options: {
        groupConfig: null, // mandatory
        // UCheckPlay preferences
        ucpStyle: null, // defaults to flickrStyle
        language: 'English',
        useLegacyLabels: false
    },
    initialize: function (options) {
        this.setOptions(options);
        var groupname = this.options.groupConfig.groupname();
        // layout style
        if (!$chk(options.ucpStyle)) {
            var storedUCPStyle = GM_getValue("UCP.ucpStyle." + groupname);
            var ucpStyle = (storedUCPStyle === true || storedUCPStyle === 'true');
            if (ucpStyle) {
                this.options.ucpStyle = new UCPStylePreferences(this.groupConfig());
            }
        }
        // language
        if (!$chk(options.language)) {
            var storedLanguage = GM_getValue("UCP.language." + groupname);
            if (storedLanguage) {
                this.options.language = storedLanguage;
            }
        }
        // legacy labels
        if (!$chk(options.useLegacyLabels)) {
            if ($chk(GM_getValue("UCP.useLegacyIcons." + groupname))) {
                this.options.useLegacyLabels = GM_getValue("UCP.useLegacyIcons." + groupname);
            }
        }
    },
    groupConfig: function () {
        return this.options.groupConfig;
    },
    setStyle: function (styleOptions) {
        var retval = false; // something changes?
        if (styleOptions.ucpStyle === true) {
            if (this.ucpStyle === null) {
                retval = true;
            }
            var oldStyle = this.options.ucpStyle;
            this.options.ucpStyle = new UCPStylePreferences(this.groupConfig(), {
                removeNew: styleOptions.ucpStyleRemoveNew,
                firstColumn: styleOptions.ucpStyleFirstColumn,
                sort: styleOptions.ucpStyleSort
            });
            this.options.ucpStyle.store(this.groupConfig());
            retval = retval || !this.options.ucpStyle.equals(oldStyle);
            GM_setValue("UCP.ucpStyle." + this.groupConfig().groupname(), true);
        }  else {
            if (this.options.ucpStyle !== null) {
                retval = true;
            }
            this.options.ucpStyle = null;
            GM_setValue("UCP.ucpStyle." + this.groupConfig().groupname(), false);
        }
        return retval;
    },
    setLanguage: function (language) {
        if (language !== this.language()) {
            GM_setValue("UCP.language." + this.groupConfig().groupname(), language);
            this.options.language = language;
            return true;
        }
        return false;
    },
    setUseLegacyLabels: function (useLegacyLabels) {
        if (useLegacyLabels !== this.options.useLegacyLabels) {
            GM_setValue("UCP.useLegacyIcons." + this.groupConfig().groupname(), useLegacyLabels);
            this.options.useLegacyLabels = useLegacyLabels;
            return true;
        }
        return false;
    },
    useLegacyLabels: function () {
        return this.options.useLegacyLabels || this.options.groupConfig.mandatoryGroupLabels();
    },
    // shortcuts
    language: function() {
        return this.options.language;
    },
    ucpStyle: function () {
        return $chk(this.options.ucpStyle);
    },
    ucpFirstColumn: function () {
        return this.ucpStyle() && this.options.ucpStyle.options.firstColumn;
    },
    ucpRemoveNew: function () {
        return this.ucpStyle() && this.options.ucpStyle.options.removeNew;
    },
    ucpSort: function () {
        return this.ucpStyle() && this.options.ucpStyle.options.sort;
    }
});

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;
        }
        if (photoNode.getAttribute('alt') === 'UCPthumbnail') {
            return false;
        }
        if (src.contains("buddyicons")) {
            return false;
        } 
        if (!src.contains("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
            var width = photoNode.getAttribute('width');
            // this.width returns bogus info if attribute 'width' is missing
            if ($chk(width) && width < 275) {
                return false;
            }
            // 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() {
    new Element('span', {
        html: 'UNIFIED&nbsp;CP&nbsp;' + CPtoolversion + '&nbsp;-&nbsp;',
        title: 'UNIFIED Challenges CheckPlay Tool ' + CPtoolversion,
    }).inject($("TopBar").getElement("td.Status"), 'top');
}

// 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;
}

    function ucpAddVotingError(ori, extra) {
        if (ori === null) {
            return extra;
        } else {
            return ori + " | " + extra;
        }
    }

var UCPVote = new Class({
    Implements: [Options],
    options: {
        chlgname: null,
        ucpThread: null,
        node: null,
        poster: null,
        voteText: null,
        votesArray: [],
        messages: [],
        inError: false,
        errorIdx: null
    },
    initialize: function(options) {
        this.setOptions(options);
        this.maxVote = 0;
        this.votedFor = 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;
    },
    node: function () {
        return this.options.node;
    },
    ucpThread: function () {
        return this.options.ucpThread;
    },
    calculateVotedFor: function (scoresAdded) { // is only called for a first vote
        if (scoresAdded) {
            for (var oIdx = 1, oLen = this.options.votesArray.length; oIdx < oLen; oIdx++) {
                if (this.options.votesArray[oIdx] > 0) {
                    this.votedFor = oIdx;
                    break;
                }
            }
        } else {
            for (var oIdx = 1, oLen = this.options.votesArray.length; oIdx < oLen; oIdx++) {
                if (this.options.votesArray[oIdx] < this.maxVote) {
                    this.votedFor = oIdx;
                    break;
                }
            }
        }
        return this.votedFor;
    },
    valid: function (previousVote, scoresAdded) {
        /*if (debug) {
            GM_log(['comparing in ' + this.options.chlgname,
                    ' ' + this.toString(),
                    ' with ',
                    previousVote.toString() ].join('\n'));
        }*/
        if (!(previousVote instanceof UCPVote)) {
            return true;
        }
        if (this.options.votesArray.length !== previousVote.options.votesArray.length) {
            this.addError("voted for " + (this.options.votesArray.length - 1) + 
                    " photos while '" + previousVote.poster().username + "' voted for " + 
                    (previousVote.options.votesArray.length - 1));
            return false;
        }
        if (this.options.votesArray.length === 0) {
            return true;
        }
        this.votedFor = 0; // photos start at 1
        //for (var oIdx = 0, oLength = this.options.votesArray.length; oIdx < oLength; ++oIdx) {
        this.options.votesArray.each( function (vote, oIdx) {
            if (oIdx === 0) {
                return;
            }
            if (isNaN(vote) || isNaN(previousVote.options.votesArray[oIdx])) {
                return;
            }
            var diff = vote - previousVote.options.votesArray[oIdx];
            if ((diff < 0 && scoresAdded) || (diff > 0 && !scoresAdded)) {
                this.addError("dropped a vote");
                this.options.errorIdx = oIdx;
                return;
            }
            if ((diff > 1 && scoresAdded) || (diff < -1 && !scoresAdded)) {
                this.addError("voted too much on same photo");
                this.options.errorIdx = oIdx;
                this.votedFor = oIdx;
                return;
            }
            if (this.votedFor > 0 && diff !== 0) {
                this.addError("misvoted");
                this.options.errorIdx = oIdx;
                return;
            }
            if (this.votedFor <= 0 && diff !== 0) {
                this.votedFor = oIdx;
            }
        }, this);
        if (this.votedFor <= 0) {
            this.addError("did not really vote");
            this.options.errorIdx = 0;
            return false;
        }
            /* ego-vote removal
            if (this.votedFor > 0 && this.votedFor > numberOfPhotos) {
                this.error = ucpAddVotingError(this.error, "'" + this.poster.username + "' voted for a non-existing photo");
                if (this.errorIdx < 1) {
                    this.errorIdx = this.votedFor;
                }
            } */
        return this.votedFor > 0 && this.options.errorIdx < 1;
    },
    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 fote found
                    scoreIdx = oIdx;
                } else {
                    if (scoreIdx !== oIdx) {
                        return false;
                    }
                }
            }
        }
        return scoreIdx !== 0; // no score found = no voting = not unanimous
    },
    isExampleVote: function (scoresAdded, neededScore) {
        if (scoresAdded) {
            return this.maxVote === 0;
        }
        // !scoresAdded
        if (this.maxVote !== neededScore) { // fast test
            return false;
        }
        for (var oIdx = 1, oLen = this.options.votesArray.length; oIdx < oLen; ++oIdx) {
            if (isNaN(this.options.votesArray[oIdx])) {
                return false;
            }
            if (this.options.votesArray[oIdx] !== neededScore) {
                return false;
            }
        }
        return true;
    },
    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) {
            retval = retval + ' ' + vote;
        }, this);
        retval = retval + ') => ' + this.maxVote;
        return retval;
    },
    showVotes: function () {
        var retval = undefined;
        this.options.votesArray.each(function (vote, oIdx) {
            if (oIdx === 0 || isNaN(vote) || vote <= 0) {
                return;
            }
            var part = oIdx + ": " + vote;
            if (this.options.errorIdx === oIdx) {
                part = "<b>" + part + "</b>";
            }
            if (retval === undefined) {
                retval = part;
            } else {
                retval = retval + ", "  + part;
            }
        }, this);
        return "(" + retval + ")";
    },
    showPicVotes: function () {
        var retval = "";
        this.options.votesArray.each(function (vote, oIdx) {
            if (oIdx === 0) {
                return;
            }
            if (!isNaN(vote) && vote > 0) {
                retval = retval + (retval.length > 0 ? ", " : "") + oIdx;
            }
        }, this);
        return "(" + retval + ")";
    },
    showPicResult: function () {
        // 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'});
    },
    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') {
                retval = retval + " - " + message.msg;
            }
        });
        return retval;
    },
    printStatus: function () {
        if (this.options.ucpThread.options.needsStatusOnChallengePage) {
            if ($chk(this.messages()) && this.messages().length > 0) {
                var pageAnchor = this.node().getElements('small').getLast();
                if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
                    pageAnchor = pageAnchor.getParent('div.ucpdiv');
                }
                if (!$chk(pageAnchor)) {
                    pageAnchor = this.node();
                }
                
                var commentAnchor = new Element('small', {
                    html: "UCheckPlayNG: "
                }).inject(new Element('div', {
                    'class': 'ucpdiv'
                }).inject(pageAnchor, 'after'));
                this.messages().each(function (message, idx) {
                    var msg = (idx > 0 ? " - " : "") + message.msg;
                    var color = message.type === 'error' ? 'red' : message.type === 'warning' ? 'orange' : '';
                    commentAnchor.adopt(new Element('span', {
                        html: msg,
                        styles: {
                            color: color
                        }
                    }));
                });
            }
        }
    }
});

function ucpCheckPhotoApproval(photo, topic) {
        // 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(/http:\/\/.*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 name, photoChecksum, checksum, version;
    try {
        var approveMatch = /UCPAapproved:([^:]*):([^:]*):([^:]*):(\d+)/.exec(approvedString);
        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(approveMatch)) {
        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) {
        // when split with a character, it can be double digits
        var reVoteMatchSplit  = "(?:\\b|\\s)(\\d+|[xX]{1})(?:\\s*)?[^xX\\d]{1}(?:\\s*)?(\\d+|[xX]{1})"; // at least 2
        // when stuck to each other, it should be single digits
        var reVoteMatchJoined = "(?:\\b|\\s)(\\d|[xX]{1})(\\d|[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})?(?:\\s*)?(\\d+|[xX]{1})?";
            reVoteMatchJoined = reVoteMatchJoined + "(\\d|[xX]{1})?";
        }
        reVoteMatchSplit  = reVoteMatchSplit  + "(?:\\s|\\b)";
        reVoteMatchJoined = reVoteMatchJoined + "(?:\\s|\\b)";
        ucpReHorizontalVoteMatch = new RegExp(reVoteMatchSplit + "|" + reVoteMatchJoined, "ig");
    }
    return ucpReHorizontalVoteMatch;
}

function ucpCreateHorizontalVote(picType, exampleVote, ucpThread, poster, challengeEntry, 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\b-,]ten[\s\b-,]/g,  10);
    replytxt = replytxt.replace(/nine/g,  9);
    replytxt = replytxt.replace(/eight/g, 8);
    replytxt = replytxt.replace(/seven/g, 7);
    replytxt = replytxt.replace(/six/g,   6);
    replytxt = replytxt.replace(/five/g,  5);
    replytxt = replytxt.replace(/four/g,  4);
    replytxt = replytxt.replace(/three/g, 3);
    replytxt = replytxt.replace(/two/g,   2);
    replytxt = replytxt.replace(/one/g,   1);
    replytxt = replytxt.replace(/nil/g,   0);
    replytxt = replytxt.replace(/zero/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
            //GM_log("found " + matchVotes);
            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 = new UCPVote({
            chlgname: ucpThread.challengeName(), 
            poster: poster, 
            voteText: horizontalVotes, 
            votesArray: hVotesArray,
            node: challengeEntry,
            ucpThread: ucpThread
        });
        if (exampleVote) {
            retval.isExampleVote = function (a,b) {
                return true;
            };
        }
        return retval;
        /*    } else {
                // No votes found in this reply 
            } */
    }
}

// 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,
        comments: [],
        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;
    },
    error: function () {
        return this.options.comments.some(function (comment) {
            if (comment.type === 'error') {
                return true;
            }
            return false;
        });
    },
    printStatus: function () {
        if (this.options.ucpThread.options.needsStatusOnChallengePage) {
            var pageAnchor = this.node().getElements('small').getLast();
            if ($chk(pageAnchor) && $chk(pageAnchor.getParent('div.ucpdiv'))) {
                pageAnchor = pageAnchor.getParent('div.ucpdiv');
            }
            if (!$chk(pageAnchor)) {
                pageAnchor = this.options.node;
            }
            var message = "UCheckPlayNG: found a photo posted by <b>" + this.poster().username + "</b>";
            var commentAnchor = new Element('small', {
                html: message
            }).inject(new Element('div', {
                'class': 'ucpdiv'
            }).inject(pageAnchor, 'after'));
            // ucpCompetitor.checkForValidPoster(); not! already done in processing of collectVotes
            this.options.comments.each(function (comment) {
                commentAnchor.adopt(
                    new Element('span', {
                        html: " - " + comment.msg,
                        styles: {
                            color: comment.type === 'comment' ? '':
                                   comment.type === 'warning' ? 'orange' :
                                   this.poster().admin ? 'orange' : 'red'
                        }
                    })
                );
            }, 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://api.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;
            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];
            }
            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 () {
        var pageAnchor = this.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 (this.options.ucpThread.challengeDefinition().scoreType() === "MEETANDGREET") {
            var message = "found a greeting from <b>" + this.poster().username;
        } else {
            if ($chk(this.options.comments) && this.options.comments.length > 0) {
                this.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 " + this.poster().username;
        }
        new Element('small', {
            html: "UCheckPlayNG: " + message + "<br/>"
        }).inject(new Element('div', { 'class': 'ucpdiv' }).inject(pageAnchor, 'after'));
    }
});

function ucpCreateCompetitor(photoNode, poster, ucpThread) {
    var ownerId;
    var photoId = photoNode.get('src').match(/http:\/\/.*flickr.com\/\d+\/(\d+)_.*/)[1]
    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,
                    comments: [ comment ],
                    poster: poster,
                    owner: owner,
                    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,
                    poster: poster,
                    owner: owner,
                    ucpThread: ucpThread
                });
                retval.addError(approvalCheck.error);
                return retval;
            }
        } else {
            return new UCPCompetitor({
                node: photoNode.getParent('td.Said').getElement('p'),
                photo: photoNode,
                photoId: photoId,
                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,
            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.getElement('p').getElements("img").filter(ucpCheckPhoto, ucpThread.challengeDefinition());
        var potentialCompetitor;
        photos.each(function(photoNode) {
            if (potentialCompetitor !== undefined && !all) {
                return;
            }
            potentialCompetitor = ucpCreateCompetitor(photoNode, poster, 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 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
                        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;
                            };
                        voteArray.include(voidedVote);
                    } else {
                        if (vVotesFound <= 1) { // voting for one photo makes no sense
                            commentArray.include(new UCPVoteComment({
                                poster: poster,
                                node: challengeEntry,
                                ucpThread: ucpThread
                            }));
                        } else {
                            var vote = new UCPVote({
                                chlgname: challengeConfig.reName(), 
                                poster: poster, 
                                voteText: vVoteLines, 
                                votesArray: vVotesArray, 
                                node: challengeEntry,
                                ucpThread: ucpThread
                            });
                            if (exampleVote) {
                                vote.isExampleVote = function (a,b) {
                                   return true;
                                };
                            };
                            voteArray.include(vote);
                        }
                    }
                    break;
                
                case "VERTICAL-WEIGHTED":
                    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-
                    var reVwVoteMatch2 = /(\d{1,2})#(\d+)/; // '01-1'
                    var reVwVoteMatch3 = /(\d{1,2})#(\d+)([^0-9azA-Z](\d+))+/; // '01-1+2' && '01- 1+2+3'
                    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(reVReplaceMatch, "$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");
                        if (vwVoteLine.match(/^\s*\d{1,2}\s*$/)) {
                            vwVoteLine = vwVoteLine.replace(/^s*(\d{1,2})\s*$/, "$1#0");
                        }
                        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 = reVwVoteMatch3.exec(vwVoteLine);
                        if (verticalVotes) {
                            photoNumber = verticalWVotes[1];
                            photoScore = verticalWVotes[4];
                            vwVotesArray[parseInt(photoNumber.replace(/^0/, ''), 10)] = parseInt(verticalWVotes[4], 10);
                            ++vwVotesFound;
                            continue;
                        }
                        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;
                            };
                        voteArray.include(voidedWVote);
                    } else {
                        if (vwVotesFound <= 1) { // voting for one photo makes no sense
                            commentArray.include(new UCPVoteComment({
                                poster: poster,
                                node: challengeEntry,
                                ucpThread: ucpThread
                            }));
                        } else {
                            var vote = new UCPVote({
                                chlgname: challengeConfig.reName(), 
                                poster: poster, 
                                voteText: vwVoteLines, 
                                votesArray: vwVotesArray, 
                                node: challengeEntry,
                                ucpThread: ucpThread
                            });
                            if (exampleVote) {
                                vote.isExampleVote = function (a,b) {
                                   return true;
                                };
                            };
                            vote.valid = function () {
                                return true;
                            };
                            voteArray.include(vote);
                        }
                    }
                    break;

                case "RATE-PHOTO":
                    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
                    var reVoteMatch1 = /(?:photo|foto)?\s*(\d+)[^\d]*(\d+)\s*po(?:i)?nt/i;
                    var reVoteMatch2 = /(\d+)\s*(?:punto|pont|point|pto)[^\d]*(\d+)/i;
                    var rVotesFound = 0;
                    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;
                        }
                    // thisvotes is still null => normal comment
                    });
                    //my_log(vVotesArray);
                    if (rVotesFound <= 0) {
                        commentArray.include(new UCPVoteComment({
                            poster: poster, 
                            node: challengeEntry,
                            ucpThread: ucpThread
                        }));
                    } else {
                        var vote = new UCPVote({
                            chlgname: challengeConfig.chlgname, 
                            poster: poster, 
                            voteText: rVoteLines, 
                            votesArray: rVotesArray, 
                            node: challengeEntry,
                            ucpThread: ucpThread
                        });
                        if (exampleVote) {
                            vote.isExampleVote = function (a,b) {
                                return true;
                            };
                        }
                        voteArray.include(vote);
                    }
                    break;
               
                case "HORIZONTAL":
                    var neededPhotos = challengeConfig.neededPhotos();
                    if (neededPhotos < 1 || neededPhotos === 65535) {
                        neededPhotos = photoArray.length;
                    }
                    var createdHVote = ucpCreateHorizontalVote(
                                        false, exampleVote, ucpThread, poster, challengeEntry, replytxt);
                    if (createdHVote) {
                        voteArray.include(createdHVote);
                    } else {
                        //GM_log(challengeConfig.options.reName + ": found another comment from user " + poster.username + ": " + replytxt);
                        var vote = new UCPVoteComment({
                            poster: poster,
                            node: challengeEntry,
                            ucpThread: ucpThread
                        });
                        if (exampleVote) {
                            vote.isExampleVote = function (a,b) {
                                return true;
                            };
                        }
                        commentArray.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 = ucpCreateHorizontalVote(true, exampleVote,
                                                    ucpThread, poster, challengeEntry, replytxt);
                            if (createdPicHVote) {
                                voteArray.include(createdPicHVote);
                            } else {
                                //GM_log(challengeConfig.options.reName + ": found another comment from user " + 
                                            //poster.username + ": " + replytxt);
                                commentArray.include(new UCPVoteComment({
                                    poster: poster,
                                    node: challengeEntry,
                                    ucpThread: ucpThread
                                }));
                            }
                        } else if (picOrientation === 'V') {
                            // 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
                            for (var k = 0, pvVoteLinesLength = pvVoteLines.length; k < pvVoteLinesLength; ++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
                                    continue;
                                }
                                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;
                                        continue;
                                    }
                                }
                            }
                            if (picX === undefined || pVotesFound <= 0) {
                                commentArray.include(new UCPVoteComment({
                                    poster: poster, 
                                    node: challengeEntry,
                                    ucpThread: ucpThread
                                }));
                            } else { // picX defined
                                if (pVotesFound < picX) {
                                    commentArray.include(new UCPVoteComment({
                                        poster: poster,
                                        node: challengeEntry,
                                        ucpThread: ucpThread
                                    }));
                                } else {
                                    var picVVote = new UCPVote({
                                        chlgname: challengeConfig.reName(), 
                                        poster: poster, 
                                        voteText: pvVoteLines, 
                                        votesArray: pvVotesArray, 
                                        node: challengeEntry,
                                        ucpThread: ucpThread
                                    });
                                    // also catches rules:
                                    // 1) ...
                                    // 2) ...
                                    if (!exampleVote) {
                                        var picExampleVote = picX > 1; // assume an example vote, only if voting for multiple
                                                                // voting for photo 1 results in example vote otherwise
                                        for (var picIdx = 1, picLen = pvVotesArray.length; picIdx < picLen; ++picIdx) { // skip index 0: 1-based voting
                                            if (!pvVotesArray[picIdx] || isNaN(pvVotesArray[picIdx])) {
                                                picExampleVote = false;
                                                break;
                                            }
                                        }
                                    }
                                    if (picExampleVote || exampleVote) {
                                        picVVote.isExampleVote = function (scoresAdded, neededScore) {
                                            return true;
                                        }; // override default behaviour
                                    }
                                    voteArray.include(picVVote);
                                }
                            }
                        } else if (picOrientation === 'P') {
                            // 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;
                            for (var l = 0, ppVoteLinesLength = ppVoteLines.length; l < ppVoteLinesLength && !ppVote; ++l) {
                                var ppVoteLine = ppVoteLines[l];
                                // (remove buddyicons first: contains @)
                                ppVoteLine = ppVoteLine.replace(/<img[^>]+>/g, '');
                                // ignore lines that are comment text
                                if (!ppVoteLine.match(/@|#|p(oi)?nt/i)) {
                                    continue;
                                }
                                // 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, '').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, '');
                                    for (var ppPhotoIdx = 0, ppPhotoLen = photoArray.length; 
                                            ppPhotoIdx < ppPhotoLen; ++ppPhotoIdx) {
                                        var photo = photoArray[ppPhotoIdx];
                                        if (!(photo instanceof UCPCompetitor)) {
                                            continue;
                                        }
                                        var challenger = photo.poster().username.replace(/(?:_|\s)+/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)ll?a/i)) ||
                                            (challenger.match(/anglerove/) && player.match(/angelrove/i))
                                            )) {
                                            ppVotesArray[ppPhotoIdx + 1] = 1; //parseInt(score, 10);
                                            ppVote = new UCPVote({
                                                chlgname: challengeConfig.reName(), 
                                                poster: poster,
                                                voteText: ppVoteLines, 
                                                votesArray: ppVotesArray, 
                                                node: challengeEntry,
                                                ucpThread: ucpThread
                                            });
                                            continue;
                                        }
                                        if (photo && challenger.toLowerCase().match(player.toLowerCase())) {
                                            ppVotesArray[ppPhotoIdx + 1] = 1; // parseInt(score, 10);
                                            ppVote = new UCPVote({
                                                chlgname: challengeConfig.reName(), 
                                                poster: poster, 
                                                voteText: ppVoteLines, 
                                                votesArray: ppVotesArray, 
                                                node: challengeEntry,
                                                ucpThread: ucpThread
                                            });
                                        }
                                    }
                                }
                            }
                            if (ppVote) {
                                voteArray.include(ppVote);
                            } else {
                                commentArray.include(new UCPVoteComment({
                                    poster: poster,
                                    node: challengeEntry,
                                    ucpThread: ucpThread
                                }));
                            }
                        }
                    }
                }
            }
        }); // each entry

        return { votes: voteArray, comments: commentArray, photos: photoArray };
    }

    function ucpProcessVotes(ucpThread, playername, votes, photosposted) {
        var doubleVotesHash = new Hash();
        var doublePlayerHash = new Hash();

        var cummulativeScore = new UCPVote({
            chlgname: null, 
            poster: "flickr", 
            voteText: "none", 
            votesArray: [], 
            commentAnchor: null,
            ucpThread: ucpThread
        });
        var previousVote = null;
        $each(votes, function (vote, j) {
            if (previousVote === null &&
                vote.isExampleVote(ucpThread.challengeDefinition().scoresAdded(), 
                                        ucpThread.challengeDefinition().neededScore())) {
                vote.addMessage("found an example vote");
                vote.printStatus();
                return;
            }
            if (vote.isVoid()) {
                vote.addMessage("found a voided vote");
                vote.printStatus();
                return;
            }
            vote.addMessage(
                        "<b>" + 
                        (
                            vote.poster().username === playername ? 
                            "you" : 
                            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 (vote.poster().username === playername) {
                ucpThread.updateStatus("voter");
            }
            if (ucpThread.challengeDefinition().scoreType().match(/PIC-[HV]/)) {
                vote.addMessage(vote.showPicVotes());
                cummulativeScore.add(vote);

            } else if (ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO/)) {
                // DO NOTHING
            
            } else if (ucpThread.challengeDefinition().scoreType().match(/VERTICAL-WEIGHTED/)) {
                // DO NOTHING

            } else { // HORIZONTAL or VERTICAL or PIC-P
                if (ucpThread.challengeDefinition().scoreType().match(/PIC-P-/)) {
                    cummulativeScore.add(vote);
                } else {
                    cummulativeScore = vote;
                }
                if (ucpThread.finished(cummulativeScore)) {
                    ucpThread.updateStatus("finished");
                }
                var currentValidVoting = true;
                if (previousVote) {
                    currentValidVoting = vote.valid(previousVote, 
                                                        ucpThread.challengeDefinition().scoresAdded());
//                                                        photos.length, debug);
                } else {
                    vote.calculateVotedFor(ucpThread.challengeDefinition().scoresAdded()); // for the first vote
                }
                if (vote.votedFor > 0) {
                    vote.addMessage("for photo " + vote.votedFor);
                }
                var votingResult = ucpThread.challengeDefinition().scoreType().match(/PIC-P/) ? 
                        cummulativeScore.showPicResult() : vote.showVotes();
                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';
                        });
                    }
                }
                vote.addMessage(votingResult);
            }
            if (ucpThread.challengeDefinition().playerVoting() === "maynotvote") {
                if (doublePlayerHash.has(vote.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();
            if (ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
                previousVote = cummulativeScore;
                previousVote.options.poster = vote.options.poster;
            } else {
                previousVote = vote;
            }
            /* TODO:
            if (vote.poster().username === playername && !vote.hasError()) {
                vote.setProvideDiscussLink(true);
            }
            */
        });
        previousVote = null;

        //overwrite some base statusses if challenge is in voting.
        if (ucpThread.challengeName().match(groupConfig.states().closed)) { // should never happen
//        GM_log("overwriting: " + ucpThread.challengeName() + " matches " + groupConfig.states().closed);
            ucpThread.updateStatus("closed");
        }
        if (ucpThread.finished(cummulativeScore)) {
            ucpThread.updateStatus("finished");
        }
        GM_log("checkStatus("+photosposted+")");
        ucpThread.checkStatus(photosposted);
        
        // TODO: if vote has just been cast (url/#comment1234..), and is ok, automatically open Discuss page

        if (ucpThread.challengeStatus() === "Finished"    || ucpThread.challengeStatus() === "PlayerFinished" || 
            ucpThread.challengeStatus() === "PlayerVoted" || ucpThread.challengeStatus() === "Voted" ||
            ucpThread.challengeStatus() === "Closed" || ucpThread.challengeStatus() === "Player") {
            if (ucpThread.challengeDefinition().scoreType().match(/PIC-[HVP]/)) {
                ucpThread.setScoreSummary(cummulativeScore.showPicResult());
            } else if (ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO|VERTICAL-WEIGHTED/)) {
                // do nothing
            } else {
                ucpThread.setScoreSummary(cummulativeScore.showVotes());
            }
        }
        //let's go and change the update status on screen
           
    }

// end former commonLibraryNG


// defaults
var playername = GM_getLoggedInUser();
//var playername = "Little_Debbie"; DEBUG
var playernumber = 0; // keep it global! passing it along as a member of groupConfig seems not to work
var groupConfig;
var groupPreferences;
var ucpLanguage;
            var reGroupnameMatch = /.*flickr.com\/groups\/([^\/.]*)\//;
            var groupname = reGroupnameMatch.exec(document.location.href)[1]; // needed for version check
var userNsid;
var adminOrMod = false;
var debug = false;

var ucpGroupConfigReader;
var ucpLanguageConfigReader;
var topicListingTable;
    
function initialize() {
    ucpGroupConfigReader = new UCPGroupConfigReader();
    try {
        groupConfig = ucpGroupConfigReader.createGroupConfig();
    } catch (e) {
        GM_log("unsupported group; aborting (" + e + ")");
        return false;
    }
    
    if (!$chk(groupConfig)) {
        GM_log("unsupported group; aborting");
        return false;
    }

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

    ucpAddCPheader();

    groupPreferences = new UCPGroupPreferences({groupConfig: groupConfig});
    ucpLanguageConfigReader = new UCPLanguageConfigReader();
    ucpLanguage = ucpLanguageConfigReader.createLanguage(groupPreferences.language(), 
                groupConfig.hasLegacyLabels() && groupPreferences.useLegacyLabels() ? 
                    groupConfig.legacyLabels() : undefined,
                groupConfig.languageOverrides());
    userNsid = GM_getGlobalNsid();
    adminOrMod = groupConfig.isGroupAdministratorOrModerator(userNsid);
    /*if (adminOrMod && $chk(groupPreferences.ucpStyle())) {
        groupPreferences.setStyle({
            ucpStyle: groupPreferences.ucpStyle(),
            ucpStyleRemoveNew: groupPreferences.ucpRemoveNew(),
            ucpStyleFirstColumn: groupPreferences.ucpFirstColumn(),
            ucpStyleSort: false // TODO? move the info row with the challenge row!
        });
    }
*/
    try {
        topicListingTable = $('Main').getElement('table.TopicListing');
    } catch (e) {
    }

    return true;
}

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

        // javascript evaluates from left to right!

        somethingChanged = groupPreferences.setStyle({
            ucpStyle: $('ucpstyleRadio').checked, 
            ucpStyleRemoveNew: $('ucpStyleRemoveNew').checked,
            ucpStyleFirstColumn: $('ucpStyleFirstColumn').checked,
            ucpStyleSort: $('ucpStyleSort').checked
        }) || somethingChanged;

        somethingChanged = groupPreferences.setLanguage($('languageSelect').value) || somethingChanged;

        if (groupConfig.hasLegacyLabels() && !groupConfig.mandatoryGroupLabels()) {
            somethingChanged = groupPreferences.setUseLegacyLabels($('legacyCheckId').checked) || somethingChanged;
        }

        if (somethingChanged) {
            window.location.reload();
        } else {
            var configDialog = $("UCPNGConfigDialogDiv");
            if (configDialog) {
                configDialog.destroy();
            }
        }
    }

    // debug
    // new groups:
    // http://www.flickr.com/groups/lamanoamiga/discuss/
    // http://www.flickr.com/groups/772776@N21/
    // Nature's Pot-of-Gold Challenge Rooms
    // http://www.flickr.com/groups/face-off/
    // http://www.flickr.com/groups/26485789@N00/
    // http://www.flickr.com/groups/like_it_or_not_challenges/
    // http://www.flickr.com/groups/grandes_temas/
    // http://www.flickr.com/groups/vaiencarar/
    // http://www.flickr.com/groups/26485789@N00/ (you versus the best)

    function toggleGroupList() {
        var groupListDialog = $("UCPNGSupportedGroupListDiv");
        if (groupListDialog) {
            groupListDialog.dispose();
        } else {
            var table = new Element('table', {
                styles: {
                    border: '0',
                    cellPadding: '5',
                    cellSpacing: '0'
                }
            });
            var rowIndex = 0;
            var row, col;
            var groupList = ucpGroupConfigReader.groupList();
            $each(groupList, function (group, groupname) {
                if (!$chk(group.hideFromSupportedGroups) || group.hideFromSupportedGroups !== true) {
                    if (rowIndex % 3 === 0) {
                        row = new Element('tr').inject(table);
                    } else {
                        col = new Element('td', {
                            html: "&nbsp;",
                            styles: {
                                borderRight: 'solid grey 1px'
                            }
                        }).inject(row);
                    }
                    rowIndex++;
                    new Element('a', {
                        href: "http://www.flickr.com/groups/" + groupname + "/discuss/",
                        html: group.name
                    }).inject(new Element('td').inject(row));
                }
            });

            new Element('button', {
                type: 'submit',
                html: 'Close',
                'class': 'CancelButt',
                events: {
                    click: toggleGroupList
                }
            }).inject(new Element('td', {
                colSpan: '8',
                align: 'right'
            }).inject(new Element('tr').inject(table)));

            var dialogDiv = new Element('div', {
                'id': "UCPNGSupportedGroupListDiv",
                styles: ucpDialogStyle
            }).adopt(table);
            $('UCheckPlayNGPreferences').getParent().adopt(dialogDiv);
        }
    }

    function togglePreferencesDialog() {
        var configDialog = $("UCPNGConfigDialogDiv");
        if (configDialog) {
            configDialog.destroy();
        } else {
            new Element('div', {
                id: "UCPNGConfigDialogDiv",
                styles: ucpDialogStyle
            }).inject($("UCheckPlayNGPreferences").getParent()).adopt(
            table = new Element('table', {
                border: '0',
                cellPadding: '5',
                cellSpacing: '0'
            }));
            // title
            new Element('tr').adopt(new Element('td', { colspan: '3', width: '100%' }).adopt(
                new Element('table', { border: '0', cellPadding: '0', cellSpacing: '0' }).adopt(
                    new Element('tr').adopt(
                        new Element('td', {
                            noWrap: 'nowrap',
                            vAling: 'top',
                            styles: {
                                fontWeight: 'bold'
                            },
                            html: "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/72157623600133810/#comment72157623475618601',
                                styles: {
                                    cursor: 'help'
                                }
                            }))
                    )
                ))).inject(table);
            // style
            new Element('tr').inject(table).adopt(
                new Element('th', {
                    colSpan: '3',
                    noWrap: 'nowrap',
                    vAling: 'top',
                    html: "Display style:",
                    styles: {
                        background: '#CFCFCF'
                    }
                }));

            new Element('tr').inject(table).adopt(
                new Element('td', {
                    noWrap: 'nowrap',
                    vAling: 'top',
                }).adopt(
                    new Element('input', {
                        type: 'radio',
                        name: 'displaystyle',
                        value: 'flickr',
                        id: 'flickrstyleRadio',
                        checked: !groupPreferences.ucpStyle(),
                        events: {
                            click: function () {
                                $('ucpStyleRemoveNew').disabled = true;
                                $('ucpStyleFirstColumn').disabled = true;
                                $('ucpStyleSort').disabled = true;
                                $('ucpStyleRemoveNew').getNext().style.color = 'grey';
                                $('ucpStyleFirstColumn').getNext().style.color = 'grey';
                                $('ucpStyleSort').getNext().style.color = 'grey';
                            }
                        }
                    }),
                    new Element('label', {
                        'for': "flickrstyleRadio",
                        html: "Flickr style"
                    })),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "The default Flickr style"
                }));

            new Element('tr').inject(table).adopt(
                new Element('td', {
                    noWrap: 'nowrap',
                    vAling: 'top'
                }).adopt(
                    new Element('input', {
                        type: 'radio',
                        name: 'displaystyle',
                        value: 'ucp',
                        id: 'ucpstyleRadio',
                        checked: groupPreferences.ucpStyle(),
                        events: {
                            click: function () {
                                $('ucpStyleRemoveNew').disabled = false;
                                $('ucpStyleFirstColumn').disabled = false;
                                $('ucpStyleSort').disabled = false;
                                $('ucpStyleRemoveNew').getNext().style.color = 'black';
                                $('ucpStyleFirstColumn').getNext().style.color = 'black';
                                $('ucpStyleSort').getNext().style.color = 'black';
                            }
                        }
                    }),
                    new Element('label', {
                        'for': "ucpstyleRadio",
                        html: "UCP style"
                    })),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "Unified CheckPlay style:"
                }).adopt(
                    new Element('ul').adopt(
                        new Element('li', {
                            html: "fixes the width of the author column"
                        }),
                        new Element('li', {
                            html: "prevents wrapping in the author column"
                        }),
                        new Element('li', {
                            html: "makes the 'Latest Post' column smaller"
                        })),
                    new Element('br'),
                    new Element('input', {
                        type: 'checkbox',
                        id: 'ucpStyleRemoveNew',
                        checked: groupPreferences.ucpStyle() ? groupPreferences.ucpRemoveNew() : true,
                        disabled: !groupPreferences.ucpStyle()
                    }),
                    new Element('label', {
                        'for': "ucpStyleRemoveNew",
                        html: "remove 'NEW' labels",
                        styles: {
                            color: !groupPreferences.ucpStyle() ? 'grey' : ''
                        }
                    }),
                    new Element('br'),
                    new Element('input', {
                        type: 'checkbox',
                        id: 'ucpStyleFirstColumn',
                        checked: groupPreferences.ucpStyle() ? groupPreferences.ucpFirstColumn() : true,
                        disabled: !groupPreferences.ucpStyle()
                    }),
                    new Element('label', {
                        'for': "ucpStyleFirstColumn",
                        html: "move the status column to the front" +
                            " (Warning: this option conflicts with existing " +
                            "CheckPlay scripts that are installed <u>after</u> UCheckPlay)",
                        styles: {
                            color: !groupPreferences.ucpStyle() ? 'grey' : ''
                        }
                    }),
                    new Element('br'),
                    new Element('input', {
                        type: 'checkbox',
                        id: 'ucpStyleSort',
                        checked: /*adminOrMod ? false :*/ groupPreferences.ucpStyle() ? groupPreferences.ucpSort() : true,
                        disabled: /*adminOrMod ||*/ !groupPreferences.ucpStyle()
                    }),
                    new Element('label', {
                        'for': "ucpStyleSort",
                        html: "move 'VOTE' challenges to the top, and move 'VOTED' and 'FINISHED' challenges to the bottom",
                        styles: {
                            color: /*adminOrMod ||*/ !groupPreferences.ucpStyle() ? 'grey' : ''
                        }
                    })
                )
            );
            // language
            new Element('tr').inject(table).adopt(
                new Element('th', {
                    colSpan: '3',
                    noWrap: 'nowrap',
                    vAling: 'top',
                    html: "Language:",
                    styles: {
                        background: '#CFCFCF'
                    }
                }));
            new Element('tr').inject(table).adopt(
                new Element('td').adopt(
                    languageSelect = new Element('select', {
                        styles: {
                            background: '#F0F0F0'
                        },
                        title: 'UCP language: select your preferred language for UCP feedback',
                        id: 'languageSelect'
                    })),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "Language for titles, and labels."
                })
            );
            var languages = ucpLanguageConfigReader.getLanguageList();
            $each(languages, function (definition, language) {
                    new Element('option', {
                        value: language,
                        selected: groupPreferences.language() === language,
                        disabled: definition === undefined,
                        title:  (definition === undefined ? 
                            'Not implemented yet. Maybe you can help us out?' : 
                            language),
                        html: language
                    }).inject($('languageSelect'));
                }
            );
            // legacy labels
            if (groupConfig.hasLegacyLabels() && !groupConfig.mandatoryGroupLabels()) {
                new Element('tr').inject(table).adopt(
                    new Element('th', {
                        colSpan: '3',
                        html: "Legacy status icons:",
                        styles: {
                            background: '#CFCFCF'
                        }
                    }));
                new Element('tr').inject(table).adopt(
                    new Element('td', {
                        noWrap: 'nowrap'
                    }).adopt(
                        new Element('input', {
                            type: 'checkbox',
                            name: 'legacyicons',
                            id: 'legacyCheckId',
                            checked: groupPreferences.useLegacyLabels()
                        }),
                        new Element('label', {
                            'for': 'legacyCheckId',
                            html: 'Use legacy status icons'
                        })),
                    new Element('td', {
                        style: {
                            fontStyle: 'italic'
                        },
                        html: "Use the same status icons as the legacy CheckPlay script for this group"
                    }));
            }
            
            new Element('tr').inject(table).adopt(
                new Element('td', {
                    colSpan: '3',
                    align: 'right'
                }).adopt(

                    new Element('button', {
                        type: 'submit',
                        'class': 'Butt',
                        html: 'OK',
                        events: {
                            click: applyPreferences
                        }
                    }),
                    document.createTextNode(" "),
                    new Element('button', {
                        type: 'submit',
                        'class': 'DeleteButt',
                        html: 'Cancel',
                        events: {
                            click: togglePreferencesDialog
                        }
                    })
                ));

            // 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('input', {
                        type: 'checkbox',
                        id: 'languageReload'
                    }),
                    new Element('label', {
                        'for': 'languageReload',
                        html: 'language&nbsp;definitions'
                    })
                ),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "The definitions for the language translations are updated once a month. With this option, you " +
                          "can update them now."
                })
            );
            new Element('tr').inject(table).adopt(
                new Element('td', { noWrap: 'nowrap' }).adopt(
                    new Element('input', {
                        type: 'checkbox',
                        id: 'groupReload'
                    }),
                    new Element('label', {
                        'for': 'groupReload',
                        html: 'challenge&nbsp;definitions'
                    })
                ),
                new Element('td', {
                    styles: {
                        fontStyle: 'italic'
                    },
                    html: "The definitions for the challenges are updated once a week. With this option, you " +
                          "can update them now."
                })
            );
            new Element('tr').inject(table).adopt(
                new Element('td').adopt(
                    new Element('button', {
                        type: 'submit',
                        'class': 'Butt',
                        html: 'update',
                        id: 'definitionsButton',
                        events: {
                            click: function (evt) {
                                // clean up some previous update actions
                                $('languageReload').getParent('td').getElements('img').destroy();
                                $('groupReload').getParent('td').getElements('img').destroy();
                                if ($('languageReload').checked) {
                                    GM_log("need to reload language defs");
                                    new Element('img', {
                                        src: updatingIcon
                                    }).inject($('languageReload').getParent('td'));
                                    ucpLanguageConfigReader.checkForUpdates(groupPreferences.language(), true, function (result) {
                                        var uploadImg = $('languageReload').getParent('td').getElement('img');
                                        uploadImg.set('src', result.stat == 'ok' ? defaultCheckIconSmall : errorIcon);
                                        uploadImg.set('title', result.stat == 'ok' ? 'updated' : result.error);
                                    });
                                }
                                if ($('groupReload').checked) {
                                    GM_log("need to reload group defs");
                                    new Element('img', {
                                        src: updatingIcon
                                    }).inject($('groupReload').getParent('td'));
                                    ucpGroupConfigReader.checkForUpdates(groupname, true, function (result) {
                                        var uploadImg = $('groupReload').getParent('td').getElement('img');
                                        uploadImg.set('src', result.stat == 'ok' ? defaultCheckIconSmall : errorIcon);
                                        uploadImg.set('title', result.stat == 'ok' ? 'updated' : result.error);
                                    });
                                }
                            }
                        }
                    })
                )
            );
        }
    }

// -----

    function cleanupUCPvariables(groupname) {
        var keyValues = GM_listValues();
        for (var keyIdx = 0, valLen = keyValues.length; keyIdx < valLen; ++keyIdx) {
            var key = keyValues[keyIdx];
            if (!$chk(groupname)) {
                if (key.match(/UCP./) && // for versions prior to 0.5.10
                    !key.match(/UCP.language/) &&
                    !key.match(/UCP.ucpStyle/) &&
                    !key.match(/UCP.ucpStyle.removeNew/) &&
                    !key.match(/UCP.ucpStyle.firstColumn/) &&
                    !key.match(/UCP.ucpStyle.sort/) &&
                    !key.match(/UCP.useLegacyIcons/) &&
                    !key.match(/UCP.lastVersionCheckTime/)) {
                    GM_deleteValue(key);
                }
            } else { // only for the given group
                if (key.match(new RegExp("UCP." + groupname))) {
                    GM_deleteValue(key);
                }
            }
        }
    }

    var UCPMarkFunction = new Class({
        Implements: [Options],
        options: {
            markAnchor: undefined,
            imgNode: undefined,
            marked: false
        },
        initialize: function (options) {
            this.setOptions(options);
        },
        handleEvent: function (e) {
            if (this.options.marked) {
                this.options.marked = false;
                this.options.imgNode.set('style', 'border: 0px; max-width: 75; max-height: 75');
                this.options.markAnchor.set('html', this.options.markAnchor.get('html').replace("marked", "mark"));
                this.options.markAnchor.style.background = "yellow";
                this.options.markAnchor.title = "click to mark";
            } else {
                this.options.marked = true;
                this.options.imgNode.set('style', 'border: 7px solid magenta; max-width: 75; max-height: 75');
                this.options.markAnchor.set('html', this.options.markAnchor.get('html').replace("mark", "marked"));
                this.options.markAnchor.set('style', 'background: magenta');
                this.options.markAnchor.title = "click to unmark";
            }
        }
    });
    
    function showPhotoSummary(photos) {
        var form = $$('table.TopicReply form textarea');
        if (!$chk(form) || form.length === 0) {
            GM_log("no form found (3)");
            return;
        }
        var helpmeButton = new Element('button', {
            'class': 'Butt',
            html: 'thumbnail view',
            events: {
                click: function () {
                    toggleThumbnailView(photos);
                }
            }
        }).inject($('Main').getElement('a[name=reply]').getParent(), 'before');
        var helplink = new Element('a', {
            target: '_blank',
            html: 'what is this?',
            href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623600133810/#comment72157623601636570',
            styles: {
                cursor: 'help'
            }
        }).inject(helpmeButton, 'after');
    }

    function toggleThumbnailView(photos) {
        if ($chk($('UCP:thumbnail_table'))) {
            $('UCP:thumbnail_table').dispose();
            $$('table.helpmeanchor').dispose();
            $$('a[name=helpme]').dispose();
        } else {
            var main = $$("body div#Main table")[0];
            var photoTableAnchor = new Element('a', {
                name: "helpme"
            });
            photoTableAnchor.inject(main, 'after');

            var photoTable = new Element('table', { id: "UCP:thumbnail_table" });
            photoTable.inject(photoTableAnchor, 'after');

            photoTable.adopt(
                new Element('tr').adopt(
                    new Element('th', {
                        colSpan: 5,
                        styles: {
                            color: "brown"
                        },
                        html:
                            "Please view the photos in medium size first.<br/>" +
                            "You will find a 'mark' link below every photo. " +
                                                   "Click the link to mark the photo's thumbnail below.<br/>" +
                            "This should make it easier to narrow down your selection."
                    }),
                    new Element('th', {
                        colSpan: 5,
                        styles: {
                            color: 'brown',
                            'text-align': 'right'
                        },
                        html:
                            "Double-check the numbers please!<br/>" +
                            "!! And don't forget to enter your vote !!"
                    })
                )
            );
            var markAnchorList = [];
            photos.each(function (competitor, photoIdx) {
                if (photoIdx % 9 === 0) {
                    photoTable.adopt(row = new Element('tr'));
                }
                // create a named anchor to link back to
                var namedAnchor, photoId;
                if (competitor.photo().getParent().get('href')) {
                    namedAnchor = competitor.photo().getParent();
                    try {
                        photoId = competitor.photo().src.match(/http:\/\/.*flickr.com\/\d+\/(\d+)_.*/)[1];
                        if (photoId) {
                            namedAnchor.name = photoId;
                        }
                    } catch (e) {
                    }
                }
                var cell;
                row.adopt(cell = new Element('td', {
                    html: photoIdx + 1
                }));
                if (namedAnchor && photoId) {
                    var photoAnchor;
                    cell.adopt(photoAnchor = new Element('a', {
                        href: '#' + photoId
                    }));
                    // trick the rest of the script
                    cell = photoAnchor;
                }
                var thumb;
                cell.adopt(thumb = new Element('img', {
                    src: competitor.photo().src,
                    alt: 'UCPthumbnail',
                    styles: {
                        'max-width': 75,
                        'max-height': 75
                    }
                }));
                // fetch the thumbnail version from Flickr? will not work for photos with all rights reserved
                // => no 'All sizes', no public thumbnail
                var markAnchor = { 
                    anchor: new Element('a', {
                        html: "mark",
                        styles: {
                            background: "yellow",
                            cursor: 'pointer'
                        },
                        title: "click to mark",
                        'class': 'helpmeanchor'
                    }),
                    photo: competitor.photo()
                };
                //markAnchor.anchor.style.textDecoration = "underline";
                var markFunction = new UCPMarkFunction({
                    markAnchor: markAnchor.anchor,
                    imgNode: thumb
                });
                markAnchor.anchor.addEventListener('click', markFunction, false);
                markAnchorList.push(markAnchor);
            });

            markAnchorList.each(function (markAnchor) {
                var div = new Element('div', { width: '100%', 'class': 'helpmeanchor' }).adopt(
                    new Element('table', {
                        'class': 'helpmeanchor',
                        width: '100%'
                    }).adopt(
                        new Element('rw').adopt(
                            new Element('td').adopt(markAnchor.anchor),
                            new Element('td', {
                                align: 'right',
                                width: '100%'
                            }).adopt(
                                new Element('a', {
                                    'class': 'toThumbnails',
                                    href: '#helpme',
                                    html: 'to the thumbnails',
                                })
                            )
                        )
                    )
                );
                if ($chk(markAnchor.photo.getParent('a'))) {
                    div.inject(markAnchor.photo.getParent('a'), 'after');
                } else {
                    // hope for the best
                    div.inject(markAnchor.photo, 'after');
                }
            });
            document.location.href = "#helpme";
        }
    }

    function processDiscussionTopic(discussionTopic, ucpThread) {
        /*var threadNr = ucpThread.challengeName().match(/^\d+/);
        if ($chk(threadNr)) {
            ucpThread.startprocessing = new Date();
        }*/
        var debug = false; //ucpThread.challengeName().match('Winter');
        var playerInThisTopic = false;
        var form = $(discussionTopic).getElement('table.TopicReply form textarea');
        if (!$chk(form)) { // closed, or not a member
            form = $(discussionTopic).getElement('table.TopicReply div.Tease');
            if (!$chk(form) || form.length === 0) {
                GM_log("no form found");
                ucpThread.resetStatus();
                ucpThread.setChallengeStatus("Closed");
                ucpThread.store();
                if (!adminOrMod) {
                    ucpThread.printStatus(groupPreferences, ucpLanguage);
                }
            }
        }
        if ($chk(form)) {
            var formFeedback = new Element('small', {
                id: 'UCPNGFormFeedback',
                styles: {
                    textDecoration: 'none'
                }
            }).inject(form, 'before');
            ucpThread.setFeedbackElement(formFeedback);
        } else if (adminOrMod) {
            var closedMessage = $('Main').getElement('table p.Focus');
            if ($chk(closedMessage)) {
                //GM_log("found the Closed message");
                var formFeedback = new Element('small', {
                    id: 'UCPNGFormFeedback',
                    styles: {
                        textDecoration: 'none'
                    }
                }).inject(closedMessage, 'before');
                ucpThread.setFeedbackElement(formFeedback);
            }
        }
        if (ucpThread.challengeDefinition().nonChallengeType()) {
            ucpThread.resetStatus();
            ucpThread.store();
            ucpThread.printStatus(groupPreferences, ucpLanguage);
            return;
        }
        var page = document.location.href.match(/\/page(\d+)/);
        var stopProcessing = false;
        if (page) {
            page = parseInt(page[1], 10);
            stopProcessing = page > 1;
        }
        ucpThread.setLastLoadTime(Math.round(CPStartTime.getTime() / 1000));
        var nonChallengeStatus;
        var challengeAnnouncement = $(discussionTopic).getElement('td.Said');
        if (!$chk(challengeAnnouncement) || challengeAnnouncement.length === 0) {
            GM_log("no challengeAnnouncement!");
            ucpThread.setChallengeStatus("ERRORPARSING");
            ucpThread.addVotingError("empty topic");
            ucpThread.store();
            ucpThread.printStatus(groupPreferences, ucpLanguage);
            return;
        }
        if (!stopProcessing) {
            if ((!$chk(form) || form.length === 0) && !ucpThread.challengeDefinition().nonChallengeType()) {
                GM_log("no form found (2)");
                ucpThread.resetStatus();
                ucpThread.setChallengeStatus("Closed");
                ucpThread.store();
                if (!adminOrMod) { // continue to show the result, errors, ..
                    ucpThread.printStatus(groupPreferences, ucpLanguage);
                    return;
                }
            }
        }

        // the announcement could contain override information:
        // <img src="..." alt="UCPoverride:photos:3"/>
        // <img src="..." alt="UCPoverride:votes:10"/>

        ucpThread.challengeDefinition().readChallengeDefinitionOverrides(challengeAnnouncement);
        // re-test !!
        if (ucpThread.challengeDefinition().nonChallengeType()) {
            ucpThread.resetStatus();
            ucpThread.store();
            ucpThread.printStatus(groupPreferences, ucpLanguage);
            return;
        }

        if (ucpThread.challengeStatus() !== "Closed") { // in case of admins, continue
            ucpThread.setChallengeStatus("none");
        }
        var photosposted = 0;

        if (!$chk(discussionTopic)) {
            GM_log("no discussion topic?");
            ucpThread.setChallengeStatus("ERRORPARSING");
            ucpThread.addVotingError("no DiscussTopic");
            ucpThread.store();
            return;
        }
        if (debug) GM_log("ucpThread: " + ucpThread.toString());
        if (debug) GM_log("challenge: " + ucpThread.challengeDefinition().toString());
        // discussionTopic contains the challenge announcement (table 1) and the replies (table 2);
        var replies = discussionTopic.getElements('table.TopicReply tbody tr');
        if (replies.length === 0) {
            GM_log("no replies yet in " + ucpThread.challengeName());
            ucpThread.resetStatus();
            ucpThread.setChallengeStatus("none");
            ucpThread.store();
            ucpThread.printStatus(groupPreferences, ucpLanguage);
            return;
        }
        // 
        if (groupConfig.excludeReplyIndexes().contains(0)) {
            ucpThread.findExcludesInDOMNode(challengeAnnouncement);
            if (ucpThread.isExcluded(playername)) {
                ucpThread.updateStatus("Excluded");
            }
        }
        // get all 'says:' elements
        var challengeEntries = discussionTopic.getElements('td.Said');
        if (!challengeEntries || challengeEntries.length === 0) { // no replies
            ucpThread.resetStatus();
            ucpThread.store();
            ucpThread.printStatus(groupPreferences, ucpLanguage);
            return;
        }
        var commentcounter = challengeEntries.length - 1;
        
        if (!stopProcessing) { // only on first page
            groupConfig.excludeReplyIndexes().each(function (index) {
                var challengeEntry = challengeEntries[index];
                if (challengeEntry && index > 0) { // already done the announcement
                    ucpThread.findExcludesInDOMNode(challengeEntry);
                    if (ucpThread.isExcluded(playername)) {
                        ucpThread.updateStatus("Excluded");
                    }
                }
            });
            ucpThread.printExcludes(challengeAnnouncement);
        }
        var replyArray = ucpThread.collectVotes(challengeAnnouncement, challengeEntries);
        var votes  = replyArray.votes;
        var comments = replyArray.comments;
        var photos = replyArray.photos;
           
        var doubleVotesHash = new Hash();
        var doublePlayerHash = new Hash();

        photosposted = photos.length;
        $each(photos, function (photo) {
            var photoposter = photo.poster().username;
            if (photoposter === playername) {
                ucpThread.updateStatus("photoposter");
                if (ucpThread.challengeDefinition().countsToLimit() && !playerInThisTopic) {
                    ++playernumber;
                    playerInThisTopic = true;
                }
            }
            if (ucpThread.challengeDefinition().limitPerPlayer() > 0) {
                if (doublePlayerHash.has(photo.poster().userid)) {
                    // this member has more than one photo entered
                    var entries = doublePlayerHash.get(photo.poster().userid) + 1;
                    if (entries > ucpThread.challengeDefinition().limitPerPlayer()) {
                        if (!photo.poster().admin) {
                            ucpThread.addVotingError("'" + photo.poster().username + "' entered more than " + 
                                            ucpThread.challengeDefinition().limitPerPlayer() + " photo(s)");
                        }
                        photo.addError("(max. photos allowed: " + ucpThread.challengeDefinition().limitPerPlayer() + ")");
                    }
                    doublePlayerHash.set(photo.poster().userid, entries);
                } else {
                    doublePlayerHash.set(photo.poster().userid, 1);
                }
            }
            //GM_log("comparing '"+excludedPlayer+"' with '"+vote.username+"'");
            if (ucpThread.excludedPlayers().contains(photo.poster().username)) {
                ucpThread.addVotingError("'" + photo.poster().username + "' was excluded, but entered anyway");
                photo.addError(photo.poster().username + " was excluded, but entered anyway");
            }
            if (!photo.checkForValidPoster()) {
                if (!photo.poster().admin) {
                    ucpThread.addVotingError("'" + photo.poster().username + "' posted a photo of someone else");
                    photo.addError("posted a photo of someone else");
                } else {
                    photo.addWarning("posted a photo of someone else");
                }
            }
            photo.printStatus();
        }); // photo loop

        // non-vote comments
        $each(comments, function (comment) {
            comment.ucpThread = ucpThread;
            if (ucpThread.challengeDefinition().scoreType() === "MEETANDGREET") {
                if (comment.poster().username === playername) {
                    ucpThread.updateStatus("voter");
                }
            }
            comment.printStatus();
        });

        // votes
        var cummulativeScore = new UCPVote({
            chlgname: null, 
            poster: "flickr", 
            voteText: "none", 
            votesArray: [], 
            commentAnchor: null,
            ucpThread: ucpThread
        });
        var previousVote = null;
        $each(votes, function (vote, j) {
            if (previousVote === null &&
                vote.isExampleVote(ucpThread.challengeDefinition().scoresAdded(), 
                                        ucpThread.challengeDefinition().neededScore())) {
                vote.addMessage("found an example vote");
                vote.printStatus();
                return;
            }
            if (vote.isVoid()) {
                vote.addMessage("found a voided vote");
                vote.printStatus();
                return;
            }
            vote.addMessage(
                        "<b>" + 
                        (
                            vote.poster().username === playername ? 
                            "you" : 
                            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 (vote.poster().username === playername) {
                ucpThread.updateStatus("voter");
            }
            if (ucpThread.challengeDefinition().scoreType().match(/PIC-[HV]/)) {
                vote.addMessage(vote.showPicVotes());
                cummulativeScore.add(vote);

            } else if (ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO/)) {
                // DO NOTHING
            
            } else if (ucpThread.challengeDefinition().scoreType().match(/VERTICAL-WEIGHTED/)) {
                // DO NOTHING

            } else { // HORIZONTAL or VERTICAL or PIC-P
                if (ucpThread.challengeDefinition().scoreType().match(/PIC-P-/)) {
                    cummulativeScore.add(vote);
                } else {
                    cummulativeScore = vote;
                }
                if (ucpThread.finished(cummulativeScore)) {
                    ucpThread.updateStatus("finished");
                }
                var currentValidVoting = true;
                if (previousVote) {
                    currentValidVoting = vote.valid(previousVote, 
                                                        ucpThread.challengeDefinition().scoresAdded(), 
                                                        photos.length, debug);
                } else {
                    vote.calculateVotedFor(ucpThread.challengeDefinition().scoresAdded()); // for the first vote
                }
                if (vote.votedFor > 0) {
                    vote.addMessage("for photo " + vote.votedFor);
                }
                var votingResult = ucpThread.challengeDefinition().scoreType().match(/PIC-P/) ? 
                        cummulativeScore.showPicResult() : vote.showVotes();
                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';
                        });
                    }
                }
                vote.addMessage(votingResult);
            }
            if (ucpThread.challengeDefinition().playerVoting() === "maynotvote") {
                if (doublePlayerHash.has(vote.poster().userid)) {
                    ucpThread.addVotingError(
                         "'" + vote.poster().username + "' voted in a challenge he/she plays in");
                    vote.addError("in a challenge he/she plays in");
                }
            }
            if (debug) GM_log("ucpThread: " + ucpThread.toString());
            vote.printStatus();
            if (ucpThread.challengeDefinition().scoreType().match(/PIC-P/)) {
                previousVote = cummulativeScore;
                previousVote.options.poster = vote.options.poster;
            } else {
                previousVote = vote;
            }
            /* TODO:
            if (vote.poster().username === playername && !vote.hasError()) {
                vote.setProvideDiscussLink(true);
            }
            */
        });
        previousVote = null;
    
        //overwrite some base statusses if challenge is in voting.
        if (ucpThread.challengeName().match(groupConfig.states().closed)) { // should never happen
         if (debug) GM_log("overwriting status: " + ucpThread.challengeName() + " matches " + groupConfig.states().closed);
            ucpThread.updateStatus("closed");
        }
        if (ucpThread.finished(cummulativeScore)) {
        if (debug) GM_log("finishing early");
            ucpThread.updateStatus("finished");
        }
        if (debug) GM_log("checking status");
        ucpThread.checkStatus(photosposted, debug);
        if (debug) GM_log("ucpThread: " + ucpThread.toString());
        
        // TODO: if vote has just been cast (url/#comment1234..), and is ok, automatically open Discuss page

        if (ucpThread.challengeStatus() === "Finished"    || ucpThread.challengeStatus() === "PlayerFinished" || 
            ucpThread.challengeStatus() === "PlayerVoted" || ucpThread.challengeStatus() === "Voted" ||
            ucpThread.challengeStatus() === "Closed" || ucpThread.challengeStatus() === "Player") {
            if (ucpThread.challengeDefinition().scoreType().match(/PIC-[HVP]/)) {
                ucpThread.setScoreSummary(cummulativeScore.showPicResult());
            } else if (ucpThread.challengeDefinition().scoreType().match(/RATE-PHOTO|VERTICAL-WEIGHTED/)) {
                // do nothing
            } else {
                ucpThread.setScoreSummary(cummulativeScore.showVotes());
            }
        }
        if (threadPage && !(ucpThread instanceof UCPUnknownThread)) {
            if (ucpThread.challengeName().match(/kanchenjunga/i)) {
                photos = ucpFindPhotos(challengeAnnouncement, ucpThread, true);
                showPhotoSummary(photos);
            } else if (ucpThread.challengeName().match(/MOTHER OF THE MONTH SEMI-FINALS/)) {
                showPhotoSummary(photos);
            } else if (ucpThread.challengeName().match(/Hall [oO]f Fame/) && 
                           groupConfig.groupname() === 'superchallenge') {
                showPhotoSummary(photos);
            } else if (groupConfig.groupname().match(/yes_or_no/)) {
                if (ucpThread.challengeName().match(/15.*contest.*game(x3|3x) winners/i) ||
                    (ucpThread.challengeName().match(/game winner/i) && ucpThread.challengeDefinition().scoreType() === "VERTICAL")) {
                    showPhotoSummary(photos);
                }
            } else if (groupConfig.groupname() === 'macro-life--challenges') {
                if (ucpThread.challengeName().match(/^\d{4}\s+COMP/)) {
                    showPhotoSummary(photos);
                }
            }
        }
        //let's go and change the update status on screen
           
        //update thread info
        ucpThread.setReplies(commentcounter);
        if (debug) GM_log("storing " + ucpThread.challengeStatus());
        ucpThread.store();
        ucpThread.printStatus(groupPreferences, ucpLanguage);
        if (debug) GM_log("status printed");
        ucpThread.sort(groupPreferences, topicListingTable);
        /*if ($chk(threadNr)) {
            ucpThread.stopprocessing = new Date();
            try {
                GM_log([ threadNr + ":",
                 "time between start of load and incoming file: " + (ucpThread.pageloaded.getTime() - ucpThread.startload.getTime()) + " millis",
                 "time between incoming file and start of processing: " + (ucpThread.startprocessing.getTime() - ucpThread.pageloaded.getTime()) + " millis",
                 "time to process: " + (ucpThread.stopprocessing.getTime() - ucpThread.startprocessing.getTime()) + " millis"
                   ].join('\n'));
            } catch (e) {
                GM_log("error in times: " + e);
            }
        } */
        return;
    }

    function getStoredThreadsForGroup(groupname) {
        var retval = new Hash();
        var reMatch = new RegExp("^UCP\\." + groupname + "\\.(\\d+)");
        $each(GM_listValues(), function(key) {
            if (key.match(reMatch)) {
                var threadInfo = GM_getObject(key);
                var topicId = key.match(reMatch)[1];
                retval.set(topicId, threadInfo);
            }
        });
        return retval;
    }

    function processTopicListingTable(topicListingTable, processCallback) {

        // this is the perfect place to clean up the local storage
        var storedThreads = getStoredThreadsForGroup(groupConfig.groupname());
    // select main table
        //var topicListingHeaderRow = document.evaluate(".//tr", topicListingTable, null, 
        //                XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
        var topicListingHeaderRow = topicListingTable.getElement('tr');
        var headerColumns = topicListingHeaderRow.getChildren('th');
        var latestPostColumn = headerColumns[3];
        if (groupPreferences.ucpStyle()) {
            latestPostColumn.width = "6%";
            latestPostColumn.set('html', latestPostColumn.get('html').replace('Post', ''));
        }
        var statusTitleCell = new Element("th", {
            styles: {
                width: "5%"
            }
        });
        if (groupPreferences.ucpFirstColumn()) {
            statusTitleCell.inject(headerColumns[0], 'before');
        } else {
            statusTitleCell.inject(headerColumns[3], 'after');
        }
        var statusTitleDiv = new Element('div', {
            styles: {
                textAlign: 'center'
            }
        }).adopt(new Element('a', {
            html: "UCP-ng",
            target: '_blank',
            href: 'http://www.flickr.com/groups/1307178@N20/discuss/72157623600133810/#comment72157623600157950',
            styles: {
                cursor: 'help'
            }
        }));

        statusTitleDiv.inject(statusTitleCell);
        
        var topicListingRows = topicListingTable.getElements('tr');
        // let's loop the table and start processing
        topicListingRows.each(function (topicRow, i) {
            if (i === 0) { // headers
                return;
            }
            if (groupPreferences.ucpStyle()) {
                topicRow.style.overflow = 'hidden';
                topicRow.style.height = '1.4em';
                topicRow.style.bottomPadding = '0px';
            }
            //var columns = document.evaluate("./td", topicRow, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
            var columns = topicRow.getChildren('td');
            var challengeColumn = columns[0];
            if (groupPreferences.ucpStyle()) {
                // create some extra space for a cleaner layout
                if (challengeColumn && groupPreferences.ucpRemoveNew()) {
                    // remove the <span class='New'>NEW</span>&nbsp;
                    //var newImg = document.evaluate("./span[@class = 'New']", challengeColumn, null, 
                    //                                XPathResult.FIRST_ORDERED_NODE_TYPE, null)
                    //    .singleNodeValue;
                    var newImg = challengeColumn.getElement('span.New');
                    if (newImg) {
                        newImg.dispose();
                        // DON'T!! disables the Admin's info button, and checkboxes, if run after Admin Tool
                        //challengeColumn.set('html', challengeColumn.get('html').replace(/&nbsp;/, ''));
                    }
                }
                var authorColumn = columns[1];
                if (authorColumn) {
                // style attributes are necessary on both the td element as on a div element
                    authorColumn.style.whiteSpace = 'nowrap';
                    authorColumn.style.overflow = 'hidden';
                    authorColumn.style.height = '1.02em';
                    authorColumn.style.width = '8em';
                    var authorColumnDiv = new Element("div", {
                        styles: {
                            whiteSpace: 'nowrap',
                            overflow: 'hidden',
                            height: '1.02em',
                            width: '8em'
                        }
                    }).inject(authorColumn);

                    authorColumn.getElements('a').each(function(authorAnchor) {
                        authorAnchor.dispose();
                        authorAnchor.inject(authorColumnDiv);
                    });
                }
                latestPostColumn = columns[3];
                if (latestPostColumn) {
                    latestPostColumn.style.whiteSpace = 'nowrap';
                    latestPostColumn.style.overflow = 'hidden';
                    latestPostColumn.set('html', latestPostColumn.get('html').replace(/ ago/, "")
                                                                           .replace(/minutes/, "min. ")
                                                                           .replace(/seconds/, "sec. "));
                }
            }
            //var topic = document.evaluate(".//a", columns[0], null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
            //            .singleNodeValue;
            var topic = challengeColumn.getElement('a');
            var chlgname = topic.textContent;
            var ucpThread = ucpCreateChallengeThread({
                chlgAnchor: topic,
                url: topic.href,
                chlgname: chlgname,
                groupConfig: groupConfig,
                needsStatusOnTopicListing: true
            });
            ucpThread.retrieve();
            // remove from list of stored threads: what's left can be removed from storage
            var topicId = topic.href.match(/.*flickr.com\/groups\/[^\/]+\/discuss\/(\d+)\//)[1];
            storedThreads.erase(topicId);
            //debug = chlgname.match('Naveesh');
            //if (debug) GM_log("retrieved " + ucpThread.challengeStatus());
            // reset
            var skipChallenge = groupConfig.skipChallenge(ucpThread);

            var counterColumn = columns[2];
            var commentcounter = parseInt(counterColumn.get('html'), 10);

            // add statusses
            var statusCell = new Element('td', {
                styles: {
                    textAlign: 'center'
                },
                vAling: 'center'
            }).adopt(myanchor = new Element('a', {
                id: "UCPNG." + topic.href,
                href: topic.href
            }));
            if (groupPreferences.ucpFirstColumn()) {
                statusCell.inject(columns[0], 'before');
            } else {
                statusCell.inject(columns[3], 'after');
            }
            ucpThread.setLabelElement(myanchor);
            ucpThread.setScoreAnchor(counterColumn);
            var loadneeded = false;
            if (!skipChallenge) // not "---"
            {
                // read UCP.http://www.flickr.com/groups/name/discuss/.lastloadtime
                var elapstime = Math.round(CPStartTime.getTime() / 1000) - ucpThread.lastLoadTime();

                // no matter what, if it closed as an Error the last time, reload!
                if (ucpThread.challengeStatus().match("ERROR")) {
                    loadneeded = true;
                } else if (!ucpThread.lastLoadTime() || elapstime > 60 * 3) {
                    //more then 3 minutes: force reload (after enough testing, this could be set to 1h)
                    //GM_log("reloading '" + ucpThread.challengeName() + "' based on elapsed time = " + (elapstime/60) + " min.");
                    loadneeded = true;
                } else {
                    if (ucpThread.challengeName() === undefined) { // sentinel not found
                        //GM_log("reloading based on missing name");
                        loadneeded = true;
                    } else {
                        if (!ucpThread.validVotingAsStored()) {
                            GM_log("reloading based on invalid voting");
                            loadneeded = true;
                        }
                        if (ucpThread.replies() !== commentcounter) {
                            //GM_log("reloading based on commentcounter");
                            loadneeded = true;
                            // some groups (The Pinnacle) reuse threads
                            // reparse fully if replies decrease
                            if (ucpThread.replies() > commentcounter) {
                                ucpThread.setChallengeStatus("none"); // remove VOTE or FINISHED
                            }
                        }
                        if (ucpThread.challengeStatus() === "UPDATING") {
                            //GM_log("reloading based on status");
                            loadneeded = true;
                        }
                    } 
                }
                // 3way reuses showroom threads for icon voting
                if (ucpThread.challengeName() !== chlgname) {
                    //GM_log("reloading based on name change");
                    loadneeded = true;
                } else if (ucpThread instanceof UCPNonChallengeThread) { 
                    loadneeded  = false;
                }
                if (loadneeded && ucpThread.validVotingAsStored() && 
                    ((ucpThread.challengeStatus() === 'Open' || 
                        ucpThread.challengeStatus() === 'OK' ||
                        ucpThread.challengeStatus() === 'Filled') && 
                        ucpThread.replies() === commentcounter &&
                        ucpThread.challengeName() === chlgname)) {
                    loadneeded = false;
                }
//DEBUG
//loadneeded = true;
                 // in case the name changed, make sure the rest of the script uses the new name
                ucpThread.setChallengeName(chlgname);
                if (loadneeded) {
                    ucpThread.printStatus(groupPreferences, ucpLanguage, "UPDATING");
                    ucpThread.resetStatus();
                    ucpThread.loadthread(groupPreferences, processCallback);
                } else {
                    var challengeConfig = ucpThread.challengeDefinition();
                    if (ucpThread.challengeStatus().match("Player") && // "PlayerVoted" || "PlayerMustVote" || "PlayerMayVote" || PlayerFinished
                        challengeConfig.countsToLimit()) {
                        ++playernumber; //when loadneeded => number gets added in loadthread
                    }
                    ucpThread.printStatus(groupPreferences, ucpLanguage);
                    ucpThread.sort(groupPreferences, topicListingTable);
                }
            } else { // "---"
                // differentiate between two types '---':
                // closed vs. unknown
                ucpThread.resetStatus();
                ucpThread.printStatus(groupPreferences, ucpLanguage);
                ucpThread.sort(groupPreferences, topicListingTable);
            }

        });
        storedThreads.getKeys().each(function (key) {
            GM_deleteValue("UCP." + groupname + '.' + key);
        });
        //GM_log("End of processing main discuss page");

        return;
    }

// *******************
// Start of processing
// *******************

    if (window.name === 'Log page') {
        return; //don't process log page
    }

// if version is newer than stored one, clear all GM variables
    var storedVersion;
    if (!$chk(GM_getValue("UCP.version"))) {
        cleanupUCPvariables();
    } else {
        storedVersion = GM_getValue("UCP.version"); // VO.5.9
        if (CPtoolversion !== storedVersion) {
            cleanupUCPvariables();
        }
    }
    GM_setValue("UCP.version", CPtoolversion);

    // special case: iframe with meta data from userscripts.org
    if (document.location.href.match('http://userscripts.org/scripts/source/' + scriptNumber + '.meta.js')) {
        storeVersion();
        return;
    }

    checkVersion();

// check if we have GM variables
    if (!$chk(GM_getValue("UCP.lastloadtime." + groupname))) {
        GM_setValue("UCP.lastloadtime." + groupname, Math.round(CPStartTime.getTime() / 1000));
    }

    // 
    // read UCP.http://www.flickr.com/groups/name/discuss/.lastloadtime
    var lastloadtime = parseInt(GM_getValue("UCP.lastloadtime." + groupname), 10) * 1000;
    var elapstime = CPStartTime.getTime() - lastloadtime;

    if (elapstime > 1000 * 60 * 60) //more than 1 hour : cleanup
    {
        // clear all GM_values for this group
        cleanupUCPvariables(groupname);
    }
    // store UCP.http://www.flickr.com/groups/name/discuss/.lastloadtime
    GM_setValue("UCP.lastloadtime." + groupname, Math.round(CPStartTime.getTime() / 1000));

    var discussPage = false;
    var threadPage = false;
    var pendingItemsPage = false;
    var supportedGroupOrPage = false;
    var groupNavigationMenu;
    if (document.location.href.match(/.*\/edit\/?/)) {
        GM_log("not processing edit page");
        return;
    } else if (document.location.href.match(/.*flickr.com\/groups\/1221507@N24\/discuss(?:\/)?$/)) { // the Syndicate
        GM_log("on syndicate page; ignoring");
        return;
    } else if (document.location.href.match(/.*flickr.com\/groups\/1221507@N24\/discuss\//)) { // some Syndicate page
        GM_log("some syndicate page; ignoring");
        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+/)) { // can be followed with '/', '/page', '/#comment', '/#reply', ...
        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;
    }

    if (discussPage === true) {
        // only append [preferences] on Discuss page
        groupNavigationMenu.adopt(
        new Element('p', {
            'class': "LinksNewP",
            id: 'UCheckPlayNGPreferences'
        })).adopt(
            new Element('span', {
                styles: {
                    fontWeight: 'bold'
                },
                html: "UCheckPlayNG preferences:"
            })).adopt(
            new Element('a', {
                name: '#',
                title: "Click to change preferences",
                styles: {
                    color: "Blue",
                    cursor: "pointer",
                },
                html: " [ " +
                      (groupPreferences.ucpStyle() ? "UCP style"  : "Flickr style") + " - " +
                      (groupPreferences.language() ? groupPreferences.language() : "English")
                      + " ] ",
                events: {
                    click: togglePreferencesDialog
                }
            }),
            new Element('a', {
                html: " [ Supported Challenge Groups ]",
                name: "#",
                title: "Click to access the list of supported challenge groups",
                styles: {
                    color: "Blue",
                    cursor: "pointer"
                },
                events: {
                    click: toggleGroupList
                }
            })
        );

    }
        // create field for messages (reached limit, not logged in, ..)
    navigation.adopt(mydiv = new Element('div', {
        styles: {
            display: 'none'
        },
        id: "UCheckPlayNGStatusDiv"
    }).adopt( new Element('p', {
        id: "UCheckPlayNGStatus",
        styles: {
            textDecoration: 'none',
            color: 'Red'
        }
    })));


    if (discussPage) {
        if (topicListingTable) {
            processTopicListingTable(topicListingTable, processDiscussionTopic);
        } else {
            GM_log("not processing (no TopicListing found!)");
        }
    } else if (threadPage) {
        // only process when started from page1
        var page = document.location.href.match(/\/page(\d+)/);
        if (page) {
            page = parseInt(page[1], 10);
        }
        if (!page || page === 1) {
            var discussionTopic = $('DiscussTopic');
            var challengeHeader = $('Main').getElement('td#GoodStuff').getElement('h2');
            var chlgname = challengeHeader.textContent;
            var thread   = document.location.href;
            var ucpThread = ucpCreateChallengeThread({
                    url: thread,
                    chlgname: chlgname,
                    groupConfig: groupConfig,
                    needsStatusOnChallengePage: true
            });
            ucpThread.retrieve();
            ucpThread.setLastLoadTime(Math.round(CPStartTime.getTime() / 1000));
            ucpThread.setScoreAnchor(challengeHeader);
            ucpThread.setChallengeName(chlgname); // the stored name may be different, and no longer valid
            ucpThread.resetStatus();
            //GM_log("test: " + ucpThread.challengeDefinition());
            // multiple pages?
            var nextButton = null;
            if ($chk($('GoodStuff').getElement('div.Paginator'))) {
                nextButton = $('GoodStuff').getElement('div.Paginator').getElement('a.Next');
            }
            if ($chk(nextButton)) {
                $('GoodStuff').getElements('div.Pages').dispose();
            }
            try {
                ucpThread.loadNextTopicPage($chk(nextButton) ? nextButton.href : null, 
                                discussionTopic, 
                                processDiscussionTopic);
            } catch (e) {
                GM_log("error loading next page: " + e);
            }
        } else {
            var ucpStatusDiv = $('UCheckPlayNGStatusDiv');
            var ucpStatus = $('UCheckPlayNGStatus');
            ucpStatus.set('html', " (no UCP checking on page " + page + ")");
            ucpStatusDiv.style.display = '';
            ucpStatus.style.color = 'red';
        }
    
    } else if (pendingItemsPage) {
    // nothing to do: for the admin script
    }

    // check library updates
    ucpGroupConfigReader.checkForUpdates(groupname);
    ucpLanguageConfigReader.checkForUpdates(groupPreferences.language());

    return;

    // *******************
    //  End of processing
    // *******************

})();

0 comments:

Post a Comment