User:Leaderboard/Gadget-EnhancedPOTY.js

Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * Enhaced POTY - convenient voting!
 * [[Help:EnhancedPOTY.js]]
 *
 * Features:
 *  Eligiblity check, assists to cast votes (from gallery and from vote-page) and vote removal (autodetection & saving in local storage or, if not present from contribs)
 *  Gallery-resize and -randomization based on the user name (seeded random)
 *  Statistics: Some tiny charts (someone has to run genStats() regulary and provide a page to save the data in)
 *  MyPOTY- a control center for each user (what (s)he has voted for, eligibility info & why I can vote here or not, language)
 *
 * Thanks to all translators!
 *
 * <nowiki>
 * jshint validation, please
 *
 * @rev 1 (2012-06-01)
 * @author
 *   [[User:Rillke]], 2012
 * @license
 *   GPL v.3
 */

/*global jQuery:false, mediaWiki:false, Geo:false, importStylesheet:false, importScript:false, wpAvailableLanguages:false, GallerySlide:false*/
/*jshint curly:false */

(function($, mw, undefined) {
'use strict';

/*if (1===1) {
	throw new Error('This script is currently not maintained. It could pose a security risk to the site or otherwise cause harm. Do not remove this line until you are know what you are doing.');
}*/

if (window.POTY || 'view' !== mw.config.get('wgAction')) return;

var poty;

// A bunch of helper functions
function isNumber(n) {
	return !isNaN(parseInt(n, 10)) && isFinite(n);
}
function firstItem(o) { for (var i in o) { if (o.hasOwnProperty(i)) { return o[i]; } } }
function firstItemName(o) { for (var i in o) { if (o.hasOwnProperty(i)) { return i; } } }
function isCanvasSupported() { var elem = document.createElement('canvas'); return !!(elem.getContext && elem.getContext('2d'));}


/********************************
**
** Translation
**
********************************/
mw.messages.set({
	'poty-poty-year':          "PC $1",
	'poty-poty-full-year':     "Picture Challenge $1",
	'poty-poty-full-commons':  "PC - Picture Challenge",
	'poty-welcome-banner':     "Welcome $1! Loading POTY $2.",
	'poty-slideshow':          "Slideshow",
	'poty-fullscreen':         "Full screen",
	'poty-fullscreen-close':   "Close full screen",
	
	'poty-report-error-h1':     "$1 experienced an error",
	'poty-report-error':        "The data that will be saved and publicly visible if you send the report: Your username, a timestamp, what the App did immediately before and \"$1\"",
	'poty-report-error-send':   "Send report",
	'poty-report-error-reset':  "Reset and Reload",
	'poty-report-error-cancel': "Cancel",

	'poty-ineligible-blocked':       "Your account is ineligible because it is blocked on Commons. You were blocked by $1 because $2 with an expiry of $3.",
	'poty-ineligible-nosul':         "Your account is ineligible because it is not attached to SUL.",
	'poty-ineligible-suleditcount':  "Your account is ineligible because you do not have $1 or more edits on any attached SUL account or on Commons.",
	'poty-ineligible-dateeditcount': "Your account is ineligible because you do not have $1 or more edits on any attached SUL account or on Commons before $2.",
	'poty-eligible': "You are eligible to vote because you have $1 edits on $2 before $3.",
	
	'poty-anonymous-no-vote-msg':   "You are currently not logged in. Only registered users can vote in PC.",
	
	'poty-vote-add':               "Vote",
	'poty-vote-remove':            "Remove vote",
	'poty-vote-stats':             "Statistics",
	'poty-vote-info':              "Info",
	'poty-vote-next-in-set':       "Next set image",
	'poty-voting-vote':            "Saving your vote",
	'poty-voting-remove-vote':     "Removing your vote",
	'poty-voting-app-error':       "Application ERROR",
	'poty-voting-edit-error':      "Edit ERROR",
	'poty-vote-nothing-to-remove': "Can\'t find your vote",
	'poty-vote-already-there':     "Already voted this image",
	'poty-vote-multiple-possible': "You may vote for more than one picture",
	'poty-vote-limited':           "You can vote for {{Plural:$1|ONE picture|$1 pictures}} in total",

	
	'poty-stats-chart-desc':     "Vote count compared to the average vote count per picture: ",
	'poty-stats-votelist':       "Votelist",
	'poty-stats-comments':       "Comments",
	'poty-stats-close-click':    "Click to close",
	
	'poty-my-poty-link':         "My Picture Challenge",
	'poty-my-poty-link-tooltip': "All about your participation in Picture Challenge $1",
	'poty-my-poty-h1':           "Picture Challenge $1 and You",
	'poty-my-poty-app-version':  "$1-Version $2",
	
	'poty-my-poty-language':     "Language",
	'poty-my-poty-eligibility':  "Eligibility",
	'poty-my-poty-votes':        "Votes",
	'poty-my-poty-state':        "Contest Status",
	'poty-my-poty-data':         "Data saved in your browser",
	
	'poty-my-poty-state-RX':     "Round $1 is running.",
	'poty-my-poty-state-novote': "There is no voting at the moment.",
	'poty-my-poty-state-g-RX':   "You are on a gallery made for round $1.",
	
	'poty-my-poty-action-language-saveoncommons': "Save language",
	'poty-my-poty-action-eligibility-recheck':    "Check again",
	'poty-my-poty-action-votes-recheck':          "Check again",
	'poty-my-poty-action-data-remove':            "Remove",
	'poty-my-poty-action-data-remove-warn':       "After removing this data $1 won\'t know what you have voted for."
	
});

poty = window.POTY = {
	/********************************
	**
	** Configuration
	**
	********************************/

	version: '0.6.4.6',
	// The key to be used to store the data in the user's browser
	storageKey: 'PC-04-15-TESTING',
	// POTY <year>
	year: 2014,
	// Will be retrieved from the template $('#potyVotingState')
	state: 'R1',
	// Required e.g. for digging the contribs to a specific date
	startDate: '2015-01-17T20:05:00Z',

	appName: 'Picture Challenge App',

	// Eligibility requirements
	required: {
		minEditCount: 50,
		editsBefore: '2015-01-01T00:00:00Z',
		editsDaysAgo: 7000,
		registeredBefore: '2015-01-01T00:00:00Z',
		includeDeleted: false
	},

	// Max votes
	maxVotes: {
		'R1': '~',
		'R2': 3,
		'novote': 0
	},
	roundCount: 2,
	
	votingFormat: '\n# [[User:%UserName%|%UserName%]]',
	// This avoids disclosing user names in AbuseFilter logs
	addVoteTemplate: '\n# [[User:{{subst:REVISIONUSER}}|]]',
	// Everything with null in it will be initialized later
	formattedVote: null,
	voteRegExp: null,
	votingSummaryAdd: '+1 POTY vote - eligible on %wiki% with %edits% edits - Vote through [[%VoteFrom%]] - [[Help:EnhancedPOTY.js|POTY App]]',
	votingSummaryRemove: '-1 POTY removing vote - Vote through [[%VoteFrom%]] - [[Help:EnhancedPOTY.js|POTY App]]',
	reportPage: 'MediaWiki talk:Gadget-EnhancedPOTY.js/auto-reports',
	
	// These sizes will be offered in the galleries
	availableImageSizes: [{w: 1776, h: 1000}, {w: 1280, h: 1024}, {w: 1024, h: 768}, {w: 750, h: 750}, {w: 600, h: 500}, {w: 400, h: 400}, {w: 335, h: 250}, {w: 300, h: 300}, {w: 250, h: 250}, {w: 200, h: 200}, {w: 180, h: 180}, {w: 150, h: 150}, {w: 120, h: 120}],
	availableImageSizesWide: [{w: 1920, h: 600}, {w: 1600, h: 350}, {w: 1280, h: 400}, {w: 800, h: 175}, {w: 600, h: 130}, {w: 400, h: 100}, {w: 300, h: 75}, {w: 200, h: 50}, {w: 100, h: 50}],
	wide: false,
	galleryBoxRenderer: $.noop,
	listSelector: 'li.gallerybox',
	containerSelector: '#potyEasyVoteEnhanced',
	gallerySelector: 'ul.gallery',
	setGallerySelector: '.poty-file-sets',
	fancyFontClass: 'com-poty-fancy-font',

	username: window.debugPOTYUserName || mw.config.get('wgUserName') || (window.Geo?Geo.IP:'') || 'anonymous',
	userlanguage: mw.config.get('wgUserLanguage'),
	ratelimit: -1 === $.inArray('autoconfirmed', mw.config.get('wgUserGroups')) ? 8 : -1,
	// For which round is this gallery?
	galleryType: (function() {
		var m = mw.config.get('wgPageName').match(/\d{4}\/(R\d)/);
		return (m && m[1]) || 'Rx';
	}()),
	votingPageRegExp: /^Commons:Picture of the Year\/\d{4}\/(R\d)\/v\/([^\/]{5,})$/,
	galleryRegExp: /^Commons:Picture of the Year\/\d{4}\/R\d\/Gallery\/(.+)/,
	pageNamespace: 2,
	pageBase: 'Photo Challenge',
	statsPage: 'User:Leaderboard/POTYStats',
	useStats: false,
	infoFromFileDescriptionPage: true,
	wiki: mw.config.get('wgDBname'),
	
	// Style & CSS
	stylesheet: 'MediaWiki:Gadget-EnhancedPOTY.css',
	logo: '//upload.wikimedia.org/wikipedia/commons/thumb/d/d4/POTY_barnstar.svg/150px-POTY_barnstar.svg.png',
	// When thumbs are resized to small sizes, text would overflow otherwise
	iconOnlyWidthThreshold: 250,

	fullScreenElement: {
		'R1': document.documentElement,
		'R2': '#content',
		'novote': document.documentElement
	},
	
	// Global functions to be used
	importScript: window.importScript,
	importStylesheet: window.importStylesheet,
	availableLanguages: window.wpAvailableLanguages || window.wgULSLanguages || {},
	wikiScript: mw.util.wikiScript(),
	
	// Variables merely for internal use
	translationLoaded: false,
	maxVotesR: 0,
	totalVoters: 0,
	voters: {},
	
	// Everything that does not need DOM-ready
	init: function () {
		// Variable set up
		poty.formattedVote = poty.getFormattedVote();
		poty.voteRegExp = poty.getVoteRegExp();
		poty.lastThumbSize = 'lastThumbSize' + poty.galleryType;
		poty.availableImageSizes.reverse();
		
		// Enqueue tasks
		poty.addTask('loadTranslation');
		poty.addTask('loadTranslationDone');
		poty.addTask('splash');
		poty.addTask('loadComponents');
		poty.addTask('launch');
		poty.nextTask();
	},
	
	/**************************
	*
	* The next bunch of Helpers
	*
	**************************/
	/** Helper - i18n and language
		@param {<dummy>} params  The first param is the message-name; as many arguments of type string or number as the message to create requires
		@return {String} the parsed message (HTML special chars should be escaped by parse)
	**/
	getMessage: function (params) {
		var args = Array.prototype.slice.call(arguments, 0);
		args[0] = 'poty-' + args[0];
		args[args.length] = poty.year;
		return mw.message.apply(this, args).parse();
	},
	/** Helper
		@param {String} stTimestamp  MediaWiki-Timestamp of format YYYY-MM-DDThh:mm:ssZ 
		@return {Object} JavaScript Date-Object
	**/
	getDateFromMWDate: function( stTimestamp ) {
		var regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{3})?Z$/;
		var m1 = stTimestamp.match(regex);
		return new Date(m1[1], m1[2]-1, m1[3], m1[4], m1[5], m1[6]); // Wer hat sich diesen Unsinn ausgedacht?
	},
	forEachRound: function(cb) {
		for (var i = 1; i <= poty.roundCount; ++i) {
			cb('R' + i);
		}
	},
	////////////////////////////
	///////// Storage //////////
	////////////////////////////
	saveData: function () {
		var r;
		// Catch jStorage errors. Odd Safari error occured on Mac.
		// something undefined in '_storage.__jstorage_meta.CRC32'
		try {
			var needsTTL = !poty.dataExists();
			r = $.jStorage.set(poty.storageKey, poty.data);
			if (needsTTL) {
				// Expires in 50d
				$.jStorage.setTTL(poty.storageKey, 4320000000);
			}
		} catch (ex) {
			r = ex;
		}
		return r;
	},
	getData: function () {
		var d = $.jStorage.get(poty.storageKey),
			def = {
				novote: {
					votes: {}
				},
				local: {
				}
			};
		poty.forEachRound(function(r) {
			def[r] = {
				votes: {}
			};
		});
		return $.extend(true, d, def);
	},
	deleteData: function() {
		try {
			return $.jStorage.deleteKey(poty.storageKey);
		} catch (ex) { 
			return ex;
		}
	},
	dataExists: function() {
		return !!$.jStorage.get(poty.storageKey);
	},
	getXPage: function(p, r, t) {
		return mw.config.get('wgFormattedNamespaces')[poty.pageNamespace] + ':' + poty.pageBase + '/' + poty.year + '/' + (r || poty.galleryType) + '/' + t + '/' + p;
	},
	getVotingPage: function (p, r) {
		return poty.getXPage(p, r, 'v');
	},
	getVotingPageURL: function (p, r) {
		return mw.util.wikiScript('index') + '?' + $.param({ title: poty.getVotingPage(p, r).replace(/ /g, '_') });
	},
	getInfoPage: function (p, r) {
		return poty.getXPage(p, r, 'i');
	},
	getCommentsPage: function (p, r) {
		return poty.getXPage(p, r, 'c');
	},
	getCommentsPageURL: function (p, r) {
		return mw.util.wikiScript('index') + '?' + $.param({ title: poty.getCommentsPage(p, r).replace(/ /g, '_') });
	},
	getFormattedVote: function () {
		return poty.votingFormat.replace(/%UserName%/g, poty.username);
	},
	getVoteRegExp: function () {
		return new RegExp(poty.mdEscapeSpecial($.escapeRE(poty.votingFormat.replace(/%UserName%/g, poty.username))), 'g');
	},
	/** Helper-Escape some special characters that $.escapeRE does not
		@param {String} string  String that's special chars should be escaped
		@return {String} Escaped string
	**/
	mdEscapeSpecial: function(string) {
		var specials = ['t', 'n', 'v', '0', 'f'];
		$.each(specials, function(i, s) {
			var rx = new RegExp('\\'+s, 'g');
			string = string.replace(rx, '\\'+s);
		});
		return string;
	},
	getFileNameFromPageName: function(pagename) {
		pagename = pagename || mw.config.get('wgPageName');
		var m = pagename.replace(/_/g, ' ').match(poty.votingPageRegExp);
		if (!m) {
			poty.log("Invalid voting page " + pagename);
			return '';
		}
		return m[2];
	},
	getGalleryTypeFromPageName: function(pagename) {
		pagename = pagename || mw.config.get('wgPageName');
		var m = pagename.replace(/_/g, ' ').match(poty.votingPageRegExp);
		if (!m) {
			poty.log("Invalid voting page " + pagename);
			return '';
		}
		return m[1];
	},
	getGalleryNameFromPageName: function(pagename) {
		if ('string' !== typeof pagename) pagename = mw.config.get('wgPageName');
		var m = pagename.replace(/_/g, ' ').match(poty.galleryRegExp);
		return m[1] || m[2] || m[3];
	},
	getStatsPage: function() {
		return poty.statsPage + poty.year + poty.galleryType + '.js';
	},
	
	/********************************
	**
	** Startup
	**
	********************************/
	loadComponents: function () {
		// TODO: Replace jquery.form with an API call // use [[MediaWiki:Gadget-SettingsManager.js]]
		mw.loader.using(['mediawiki.util', 'ext.gadget.libAPI', 'ext.gadget.libUtil', 'ext.gadget.jquery.fullscreen', 'ext.gadget.math.seedrandom', 'ext.gadget.SettingsManager', 'jquery.jStorage', 'jquery.spinner', 'jquery.ui', 'jquery.client'], poty.nextTask);
	},
	relaunch: function() {
		poty.tasks = [];
		setTimeout(poty.launch, 200);
	},
	launch: function () {
		poty.data = poty.getData();
		
		// delete test data, to be removed soon
		$.jStorage.deleteKey('2011POTY');
		$.jStorage.deleteKey('2012POTY_test');
		$.jStorage.deleteKey('2012POTY');
		$.jStorage.deleteKey('2014-POTY');
		$.jStorage.deleteKey('2014VVA-n');
		$.jStorage.deleteKey('2013POTY_x');
		
		poty.dataExist = poty.dataExists();
		poty.wide = !!$('#potyWide').length;
		if (poty.wide) {
			poty.availableImageSizes = poty.availableImageSizesWide.reverse();
		}
		
		
		
		poty.state = $('#potyVotingState').text().match(/during(R\d)/);
		poty.state = ( poty.state ? poty.state[1] : 'novote' );
		poty.maxVotesR = poty.maxVotes[poty.state];
		
		var msgToShow = 'poty-full-commons';
		
		switch (poty.maxVotesR) {
			case '~':
				if (poty.state === poty.galleryType) msgToShow = 'vote-multiple-possible';
				break;
			case 0:
				// Show default message
				break;
			default:
				if (poty.state === poty.galleryType) msgToShow = 'vote-limited';
				break;
		}
		poty.showBannerMessage([msgToShow, poty.maxVotesR]);
		
		if (poty.isVotingPage) {
			poty.galleryType = poty.getGalleryTypeFromPageName();
		}
		
		if (mw.user.isAnon()) {
			return poty.showAnonWarning();
		}

		poty.addTask('checkLocal');
		poty.addTask('checkLanguage');

		if (!poty.data.eligible && !poty.data.ineligible) {
			poty.currentEditGroup = -1;
			poty.queriesRunning = 0;
			poty.addTask('checkSUL');
			poty.addTask('getDbList');
			if (poty.required.includeDeleted) {
				poty.addTask('checkLocalContribs');
				poty.addTask('checkGlobalContribs');
			} else {
				poty.addTask('checkLocalExistingContribs');
				poty.addTask('checkGlobalExistingContribs');
			}
		}

		poty.addTask('showEligible');
		poty.nextTask();
	},
	
	/********************************
	**
	** UI: Gallery, styling
	**
	********************************/
	splash: function () {
			poty.$gallery = $(poty.containerSelector).find(poty.gallerySelector);
			poty.$gallery.$oldParent = poty.$gallery.parent();
			poty.$setGalleries = $(poty.setGallerySelector);
			poty.$myPotyPanelContainer = $('#com-my-poty-panel-container');
			poty.fancyFontClass = $('#potyFontSupported').text() ? poty.fancyFontClass : '';
			
			var l = poty.$gallery.find(poty.listSelector).length;
			if (l < 2 || l > 250) {
				if ($('#potyEasyVoteEnhancedButton').length){
					poty.isVotingPage = true;
					document.title = poty.getFileNameFromPageName() + ' - ' + poty.getMessage('poty-full-commons');
				} else if ($('#votesByDate').length) {
					return poty.secureCall('advancedStats');
				} else if (poty.$myPotyPanelContainer.length) {
					return poty.secureCall('myPOTYPanelPage');
				} else {
					return poty.log('Startup aborted', l);
				}
			} else {
				document.title = ($('#poty-gallery-heading').text() || poty.getGalleryNameFromPageName()) + ' - ' + poty.getMessage('poty-full-commons');
			}
			if (!mw.loader.getState('ext.gadget.POTYEnhancements.core')) {
				poty.importStylesheet(poty.stylesheet);
			}
			poty.secureCall('showSplash'); 
	},
	showSplash: function() {
			mw.loader.using(['ext.gadget.jquery.blockUI', 'jquery.client', 'jquery.ui', 'mediawiki.jqueryMsg'], function () {
				poty.secureCall('_showSplash'); 
			});
	},
	_showSplash: function () {
		var $loadBanner = poty.$loadBanner = $('<div>').hide().text(poty.getMessage('welcome-banner', poty.username));
		var $loadImage  = poty.$loadImage  = $('<img>', { src: poty.logo });
		var $loadBannerOuter = poty.$loadBannerOuter = $('<div>', { style: 'font-size:2em; padding:0.5em; min-height:1.15em;' })
			.append($loadImage, $loadBanner);

		poty.secureCall('showLoader');
		
		setTimeout(function () {
			$loadBanner.hide().fadeIn(1000);
		}, 100);

		poty.nextTask();
	},
	showLoader: function () {
		if (poty.loaderShown) return;
		poty.loaderShown = true;
		if (poty.loaderHideTimeout) clearTimeout(poty.loaderHideTimeout);
		if (poty.$loadBanner.parent().hasClass('ui-effects-wrapper')) poty.$loadBanner.parent().css('height', 'auto');
		
		$.blockUI({
			message: poty.$loadBannerOuter,
			blockMsgClass: 'poty-splash-foreground blockMsg ' + poty.fancyFontClass
		});
		var clnt = $.client.profile();
		if ('msie' === clnt.name && clnt.versionNumber >= 9) {
			$('.poty-splash-foreground').addClass('ms-ie-9-nukefilter');
		}
		$('.blockOverlay').addClass('poty-splash-background');
	},
	hideLoader: function () {
		poty.loaderHideTimeout = setTimeout(function() {
			poty.$loadBanner.stop().clearQueue().removeAttr('style');
			$.unblockUI({ fadeOut: 1500, onUnblock: function() { if (poty.$loadBanner.parent().hasClass('ui-effects-wrapper')) poty.$loadBanner.parent().css('height', 'auto'); } });
			poty.loaderShown = false;
		}, 200);
	},
	showBannerMessage: function (msg) {
		poty.$loadBanner.hide('clip', {}, 1500, function () {
			$(this).text(poty.getMessage.apply(this, msg));
		}).show('clip', {}, 1000).delay(1500);
	},
	showImmediateBannerMessage: function (msg) {
		if (poty.$loadBanner.parent().hasClass('ui-effects-wrapper')) poty.$loadBanner.unwrap(poty.$loadBanner.parent());
		poty.$loadBanner.stop().clearQueue().removeAttr('style').text(poty.getMessage.apply(this, msg)).show();
	},
	mdCreatejIcon: function (iconClass) {
		return $('<span>', { 'class': 'ui-icon ' + iconClass + ' md-inline-icon', text: ' ' });
	},
	createNotifyArea: function(textNode, icon, state) {
		return $('<div>', { 'class': 'ui-widget' }).append(
			$('<div>', { 'class': state + ' ui-corner-all', style: 'margin-top:5px; padding:0.7em;' }).append($('<p>').append(
				this.mdCreatejIcon(icon).css('margin-right', '.3em'), textNode
			))
		);
	},
	showAnonWarning: function () {
		var dlgBtns = {};
		var $logInNode = $('#pt-login');
		dlgBtns[$logInNode.text() || 'LogIn'] = function() {
			var logInLink = ($logInNode.find('a').length ? $logInNode.find('a').attr('href') : $logInNode.attr('href')) || mw.util.wikiScript() + '?' + $.param({ title: 'Special:UserLogin', returnto: mw.config.get('wgPageName') });
			window.location = logInLink;
		};
		$('<div>').text(poty.getMessage('anonymous-no-vote-msg')).dialog({
			title: poty.getMessage('poty-full-commons'),
			buttons: dlgBtns,
			modal: true,
			open: function() {
				// Look out for http://bugs.jqueryui.com/ticket/6830 / jQuery UI 1.9 / fixed in 1.10.0
				var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
				$buttons.eq(0).button({ icons: { primary: 'ui-icon-key' } }).addClass('ui-button-green');
				poty.hideLoader();
			},
			close: function() {
				poty.hideLoader();
			}
		});
		return;
	},
	/********************************
	**
	** Sets
	**
	********************************/
	parseSets: function(currentThumbSize, currentThumbSizeWidth) {
		var sets = poty.setFiles = {},
			$sets = poty.$setGalleries;
		
		$sets.find('ul.gallery').each(function(i, ul) {
			var set = { $ul: null, members: [] },
				$ul = set.$ul = $(ul);

			$ul.find('div.thumb a.image img:first-child').each(function(x, img) {
				var $el = $(img),
					// We cannot use .height() because this return 0 in webkit for detached elements
					h = Number($el.attr('height')) || parseFloat($el.css('height')),
					w = Number($el.attr('width')) || parseFloat($el.css('width')),
					$gb = $el.parents(poty.listSelector),
					src = $el.attr('src') || $el.attr('poster'),
					fileName = mw.libs.commons.titleFromImgSrc(src),
					imgInfo = poty.images[fileName] = { el: $el, sizes: {}, name: fileName, $gb: $gb };
				imgInfo.sizes[currentThumbSize] = { url: src, h: h, w: w };
				poty.arrImgs.push('File:' + fileName);
				$el.data('potyInfo', imgInfo);
				$gb.addClass('poty-element').css('width', currentThumbSizeWidth+25).children('div').css('width', currentThumbSizeWidth+25).children('div.thumb').css('width', currentThumbSizeWidth+20);
				
				set.members.push({ name: fileName, $gb: $gb, $el: $el });
				sets[fileName] = set;
			});
		});
		setTimeout(function() {
			// Do this the next time being idle ...
			$sets.hide();
		}, 5);
		return sets;
	},
	setRenderer: function($gb, $el, src, fileName) {
		// Turn sets into "image stacks"
		var set = poty.setFiles[fileName];
		if (!set) return;
		
		$.each(set.members, function(i, setItem) {
			if (setItem.name === fileName) {
				set.coverImage = i;
				set.currentIndex = i;
			}/* else {
				// Keep them hidden but they are curated by POTY app like any other candidate
			}*/
		});
		
		// Add set-indicators
		poty.images[fileName].set = set;
		$gb.addClass('poty-set-candidate');
	},
	nextInSet: function() {
		var $b         = $(this),
			$gb         = $b.closest(poty.listSelector),
			fileName    = $b.data('potyImgName'),
			img         = poty.images[fileName],
			set         = img.set,
			idx         = set.currentIndex,
			idxNew      = (idx + 1) % set.members.length,
			$currentTh  = $gb.find('div.thumb'),
			$currentCont= $currentTh.parent(),
			$nextTh     = set.members[idxNew].$gb.find('div.thumb');
			
		// Transfer file currently shown to hidden set-store
		$currentTh.prependTo(set.members[idx].$gb.children('div').first().empty());
		$currentCont.prepend($nextTh);
		
		set.currentIndex = idxNew;
	},
	resetSetPositions: function() {
		$.each(poty.setFiles, function(fn, set) {
			if (set.coverImage === set.currentIndex) return;
			
			// First, transfer the file currently shown back to hidden set-store
			var coverImg = set.members[set.coverImage],
				$candidateTh = poty.images[coverImg.name].$gb.find('div.thumb'),
				$candidateCont = $candidateTh.parent();
				
			$candidateTh.prependTo(set.members[set.currentIndex].$gb.children('div').first());
			
			// Then transfer the cover-image to its default position
			coverImg.$gb.find('div.thumb').prependTo($candidateCont);
			set.currentIndex = set.coverImage;
		});
	},
	
	
	setUpGallery: function () {
		var $gallery = poty.$gallery;
		var $imgs = $gallery.find('div.thumb a.image img:first-child');
		// Support for videos
		$imgs = $imgs.add($gallery.find('div.thumb video:first-child')).add($gallery.find('div.thumb img.playerPoster')).add($gallery.find('div.thumb div.PopUpMediaTransform img'));
		
		// Remove styling from gallery (important on panorama pages)
		$gallery.removeAttr('style');
		
		// Current thumb size and set up vars we need (perhaps, if the user wants them) later
		var currentThumbSize = poty.newSize = 5;
		var currentThumbSizeWidth = $imgs.attr('width');
		poty.resizeRunning = false;
		
		$imgs.each(function(i, el) {
			var $el = $(el),
				w = $el.attr('width') || $el[0].width || 0;
			currentThumbSizeWidth = Math.max(currentThumbSizeWidth, w);
		});
		
		var lastDiff = Infinity;
		$.each(poty.availableImageSizes, function(i, s) {
			var currentDiff = Math.abs(s.w-currentThumbSizeWidth);
			if (currentDiff < lastDiff) {
				poty.newSize = currentThumbSize = i;
				lastDiff = currentDiff;
			} else {
				return false;
			}
		});
		
		poty.images = {};
		poty.arrImgs = [];
		poty.secureCall('parseSets', currentThumbSize, currentThumbSizeWidth);
		
		$imgs.each(function(i, el) {
			var $el = $(el),
				// We cannot use .height() because this return 0 in webkit for detached elements
				h = Number($el.attr('height')) || parseFloat($el.css('height')),
				w = Number($el.attr('width')) || parseFloat($el.css('width')),
				$gb = $el.parents(poty.listSelector),
				src = $el.attr('src') || $el.attr('poster'),
				fileName = mw.libs.commons.titleFromImgSrc(src),
				imgInfo = poty.images[fileName] = { el: $el, sizes: {}, name: fileName, $gb: $gb };
			imgInfo.sizes[currentThumbSize] = { url: src, h: h, w: w };
			poty.arrImgs.push('File:' + fileName);
			$el.data('potyInfo', imgInfo);
			$gb.addClass('poty-element').css('width', currentThumbSizeWidth+25).children('div').css('width', currentThumbSizeWidth+25).children('div.thumb').css('width', currentThumbSizeWidth+20);
			try {
				poty.galleryBoxRenderer($gb, $el, src);
				poty.setRenderer($gb, $el, src, fileName);
			} catch (ex) {
				poty.log(ex);
			}
		});
		
		// Resize-slider and input
		poty.viewContainer = $('<div>', { id: 'potyViewContainer' });
		var $sizeContainer = $('<div>', { id: 'potyResizeContainer', 'class': 'poty-element' }).appendTo(poty.viewContainer);
		var $sizeInput = $('<input>', { 
			type: 'text', 
			size: 5, 
			id: 'sizeInput', 
			'class': 'numbersOnly', 
			value: poty.availableImageSizes[currentThumbSize].w 
		}).appendTo($sizeContainer)
			.bind('input change keyup', function() {
				// IE hack: Otherwise change does not fire
				var valNew = this.value.replace(/[^0-9]/g,'');
				if (valNew !== this.value) this.value = valNew;
			})
			.keyup(function(e) {
				if (13 === e.keyCode-0) $(this).triggerHandler('change');
			})
			.change(function() {
				var tVal = this.value;
				if (!tVal) return;
				var lastDiff = Infinity;
				var nearestVal = currentThumbSize;
				$.each(poty.availableImageSizes, function(i, s) {
					var thisdiff = Math.abs(s.w - tVal);
					if (lastDiff > thisdiff) {
						nearestVal = i;
					} else {
						return false;
					}
					lastDiff = thisdiff;
				});
				this.value = poty.availableImageSizes[nearestVal].w;
				$sizeSlider.slider('option', 'value', nearestVal);
			});
		var $sizeSlider = poty.$sizeSlider = $('<div>', { css: { 'display': 'inline-block', width: '250px', margin: '10px' } })
			.slider({ 
				max: poty.availableImageSizes.length-1, 
				animate: true,
				change: function(e, ui) {
					$sizeInput.val( poty.availableImageSizes[ui.value].w );
					poty.secureCall('resizeThumbs', ui.value);
				},
				slide: function(e, ui) {
					$sizeInput.val( poty.availableImageSizes[ui.value].w );
				},
				value: currentThumbSize
			})
			.appendTo($sizeContainer.append(' px<br/>'));
		var $oldSizeContainer = $('span.thumb-size-bar');
		$('<label>', { 'for': 'sizeInput', text: $oldSizeContainer.find('.thumb-size-text').text() + ' ' }).prependTo($sizeContainer);
		$oldSizeContainer.after(poty.viewContainer).hide();
		
		var lastUsed = poty.data[poty.lastThumbSize];
		if ($.isNumeric(lastUsed) && (lastUsed-0) !== currentThumbSize && !poty.wide) {
			$sizeSlider.slider('option', 'value', lastUsed);
		} else {
			poty.nextTask();
		}
	},
	installSlideshow: function () {
		var $buttons = $('<span>').attr('id', 'potyViewButtons'),
			slideshowVisiblityChanged = $.noop(),
			$fullscreen,
			$slideshow,
			currentFullscreenElement;	
			
		var startSlideshow = function (o, cont, screenread) {
			if (cont) o.cont = cont;
			if (screenread) {
				o.readFromScreen = true;
				o.readFromScreenSmallImages = true;
				o.autoPlay = true;
				o.remoteUse = true;
			}
			o.start();
		};
		var loadSlideshowAndStart = function (cont, screenread) {
			if ('object' === typeof window.GallerySlide) {
				if ($.isFunction(GallerySlide.toggleVisibility)) {
					GallerySlide.toggleVisibility();
				} else {
					startSlideshow(window.GallerySlide, cont, screenread);
				}
			} else {
				$(document).bind('slideshow', function (e, st, o) {
					// If the code requires debugging, you can uncomment the following line
					poty.log('Slideshow> ' + st);
					if ('codeLoaded' === st && o) {
						startSlideshow(o, cont, screenread);
					}
					if ('visibility' === st && o) {
						slideshowVisiblityChanged(o);
					}
				});
				poty.log('loading Slideshow');
				poty.importScript('MediaWiki:GallerySlideshow.js');
				poty.importStylesheet('MediaWiki:Gadget-GallerySlideshow.css');
			}
		};

		
		if ($.FullScreenSupported) {
			var oldCSS = '';
			var _goFullScreen = function(prevent, elem, css) {
				var $elem;
				if (false !== prevent) {
					elem = elem || poty.fullScreenElement[poty.galleryType];
					$elem = $(elem);
					if (elem !== document.documentElement) {
						oldCSS = oldCSS || {
							overflow: $elem.css('overflow'),
							margin: $elem.css('margin')
						};
						$elem.css(css || {
							'overflow': 'auto',
							'margin': 0
						});
					}
					$elem.requestFullScreen();
					currentFullscreenElement = elem;
				}
				
				var $el = $fullscreen;
				$el.unbind('click', _fullScreen).click(_closeFullScreen);
				$el.button({ label: poty.getMessage('fullscreen-close'), icons: { primary: 'ui-icon-newwin' } });
			};
			var _fullScreen = function(prevent) {
				_goFullScreen(prevent);
			};
			var _closeFullScreen = function(prevent) {
				var elem;
				if (false !== prevent) {
					$.FullScreen.cancelFullScreen();
				}
				elem = poty.fullScreenElement[poty.galleryType];
				if (elem !== document.documentElement) {
					$(elem).css(oldCSS);
				}
				
				var $el = $fullscreen;
				$el.unbind('click', _closeFullScreen).click(_fullScreen);
				$el.button({ label: poty.getMessage('fullscreen'), icons: { primary: 'ui-icon-extlink' } });
			};
			$(document).fullScreenChange(function() {
				if ($.FullScreen.isFullScreen()) {
					_fullScreen(false);
				} else {
					_closeFullScreen(false);
				}
			});
			slideshowVisiblityChanged = function( $elem ) {
				if ($elem.css('display') === 'none'  && $.FullScreen.isFullScreen()) {
					_closeFullScreen();
				}
			};
			$(mw).on('EmbedPlayerUpdateDependencies', function() {
				if (currentFullscreenElement !== document.documentElement && $.FullScreen.isFullScreen()) {
					_closeFullScreen();
				}
			});
			$fullscreen = $('<button type="button" role="button" id="potyFullscreenButton">').text(poty.getMessage('fullscreen'))
				.button({ icons: { primary: 'ui-icon-extlink' } })
				.click(_fullScreen)
				.appendTo($buttons);
		}
		$slideshow = $('<button type="button" role="button" id="potySlideshowButton">').text(poty.getMessage('slideshow'))
			.button({ icons: { primary: 'ui-icon-play' } })
			.click(function() {
				// Slideshow needs document to be fullscreen
				// otherwise it's hidden
				if ($fullscreen) {
					var requiredElem = document.documentElement;
					if (currentFullscreenElement !== requiredElem && $.FullScreen.isFullScreen()) {
						_closeFullScreen();
					}
					if(!$.FullScreen.isFullScreen()) {
						_goFullScreen(null, requiredElem, {
							'overflow': 'hidden'
						});
					}
				}
				loadSlideshowAndStart(0, true);
			})
			.appendTo($buttons);
		if ($fullscreen) $buttons.buttonset();
		$buttons.appendTo(poty.viewContainer);
		
		poty.nextTask();
	},
	imgsToQuery: [],
	resizeThumbs: function (newSize) {
		if (poty.resizeRunning) return;
		poty.$sizeSlider.slider( 'option', 'disabled', true );
		poty.resetSetPositions();
		poty.imageSizeInCache = newSize in firstItem(poty.images).sizes;
		poty._resizeThumbs(newSize);
	},
	_resizeThumbs: function (newSize) {
		if (poty.resizeRunning) return;
		poty.resizeRunning = true;
		poty.newSize = newSize;
		if (!poty.wide) poty.data[poty.lastThumbSize] = newSize;
		if (poty.imageSizeInCache) {
			return poty.secureCall('resizeThumbsCb');
		}
		if (0 === poty.imgsToQuery.length) poty.imgsToQuery = poty.arrImgs;
		var toQuery = poty.imgsToQuery.slice(0,50);
		poty.imgsToQuery = poty.imgsToQuery.slice(50);
		
		poty.queryAPI({
			action: 'query',
			prop: 'imageinfo',
			iiprop: 'url',
			iiurlwidth: poty.availableImageSizes[newSize].w,
			iiurlheight: poty.availableImageSizes[newSize].h,
			titles: toQuery.join('|'),
			requestid: newSize
		}, 'resizeThumbsCb', undefined, 'POST');
		if (!poty.loaderShown) {
			poty.showImmediateBannerMessage(['poty-year']);
			poty.showLoader();
		}
	},
	resizeThumbsCb: function (r) {
		var newSize = r ? r.requestid : poty.newSize;
		var wasReady = true;
		if (poty.imgsToQuery.length) {
			// If there are images to query in the pipe, 
			// immediately query the remaining ones to speed-up execution
			wasReady = poty.resizeRunning = false;
			poty._resizeThumbs(poty.newSize);
		}
		var doResize = function(img) {
			// Prepare for resizing
			var newFullW = poty.availableImageSizes[newSize].w,
				newFullH = poty.availableImageSizes[newSize].h,
				$galleryBox = img.$gb;
				
			// Resize the gallerybox
			var $vDiv = $galleryBox.css('width', newFullW+25).children('div').css('width', newFullW+25).children('div.thumb').css('width', newFullW+20).children('div');
			
			if (!(newSize in img.sizes)) return;
			var newW = img.sizes[newSize].w;
			var newH = img.sizes[newSize].h;
			
			// Video support
			var $player = $galleryBox.find('div.mwPlayerContainer');			
			if ($player.length) {
				$galleryBox.find('div.mwPlayerContainer').css('width', newW).css('height', newH);
			}
			var $miniPreview = $galleryBox.find('div.PopUpMediaTransform');
			$miniPreview.removeAttr('style');
			
			
			img.el.attr('src', img.sizes[newSize].url);
			img.el.css('width', newW).attr('width', newW);
			img.el.css('height', newH).attr('height', newH);
			$vDiv.css('margin', Math.round(newFullH+20-newH)/2 + 'px auto');
		};
		
		if (r) {
			var pgs = r.query.pages;
			$.each(pgs, function(idx, pg) {
				if (!pg.imageinfo) return;
				var ii = pg.imageinfo[0],
					img = poty.images[pg.title.replace('File:', '')];
					
				// Save result
				img.sizes[newSize] = { url: ii.thumburl, h: ii.thumbheight, w: ii.thumbwidth };
				doResize(img);
			});
		} else if (poty.imageSizeInCache) {
			$.each(poty.images, function(i, img) {
				doResize(img);
			});
		}
		if (wasReady) {
			poty.resizeRunning = false;
			poty.$sizeSlider.slider( 'option', 'disabled', false );
			poty.saveData();
			if (poty.availableImageSizes[poty.newSize].w < poty.iconOnlyWidthThreshold) {
				if (poty.buttons) poty.buttons.$text.hide();
			} else {
				if (poty.buttons) poty.buttons.$text.show();
			}
			if (poty.tasks.length) {
				poty.nextTask();
			} else {
				poty.hideLoader();
			}
		}
	},
	maybeDisableButtons: function() {
		// Enforcing x vote count
		if (isNumber(poty.maxVotesR) && poty.getCurrentVoteCount() >= poty.maxVotesR) {
			poty.buttons.$button.find('.poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', true);
		}
	},
	galleryButtonRenderer: function( statsLabel, setLabel, $voteButtonText, $statusButtonText, $setButtonText ) {
		$.each(poty.images, function(i, img) {
			var label = poty.data[poty.galleryType].votes[i] ? poty.getMessage('vote-remove') : poty.getMessage('vote-add'),
				$galleryBox = img.$gb,
				$galleryText = $galleryBox.find('div.gallerytext'),
				oldText = $galleryText.text(''),
				$vbt = $voteButtonText.clone().text(label),
				$sbt = $statusButtonText.clone(),
				$setbt = $setButtonText.clone(),
				$votingButton = $('<button>', { css: { 'float': 'left' }, title: label })
					.prepend($vbt)
					.prepend($('<span>', { 'class': (poty.data[poty.galleryType].votes[i] ? 'poty-ui-icon-vote-minus' : 'poty-ui-icon-vote-plus') + ' poty-icon md-inline-icon' }))
					.button({ disabled: poty.state !== poty.galleryType || !poty.data.eligible }).appendTo($galleryText).data('potyImgName', i).click(poty.voteThroughGallery),
				$statsButton = $('<button>', { css: { 'float': 'right' }, title: statsLabel })
					.prepend($sbt)
					.prepend($('<span>', { 'class': (poty.useStats ? 'poty-ui-icon-vote-stats' : 'ui-icon ui-icon-info') + ' poty-icon md-inline-icon' }))
					.button().appendTo($galleryText).data('potyImgName', i).click(poty.showStatsOrInfo),
				$setButton = $();
				
			if (img.set) {
				$setButton = $('<button>', { css: { 'float': 'right' }, title: setLabel })
					.prepend($setbt)
					.prepend($('<span>', { 'class': 'ui-icon ui-icon-carat-2-n-s poty-icon md-inline-icon' }))
					.button().appendTo($galleryText).data('potyImgName', i).click(poty.nextInSet);
			}

			// jQuery UI destroys the reference to $vbt
			poty.buttons.$button = poty.buttons.$button.add($votingButton);
			poty.buttons.$text = poty.buttons.$text
				.add($votingButton.find('.poty-vote-button-text'))
				.add($statsButton.find('.poty-vote-button-text'))
				.add($setButton.find('.poty-vote-button-text'));
		});
		if (poty.availableImageSizes[poty.newSize].w < poty.iconOnlyWidthThreshold) poty.buttons.$text.hide();
	},
	setUpButtons: function() {
		poty.buttons = {
			$button: $(),
			$text: $()
		};
	
		var statsLabel = poty.getMessage(poty.useStats ? 'vote-stats' : 'vote-info'),
			setLabel = poty.getMessage('vote-next-in-set'),
			$voteButtonText   = $('<span>', { 'class': 'poty-vote-button-text' }),
			$statusButtonText = $('<span>', { 'class': 'poty-vote-button-text', text: statsLabel }),
			$setButtonText    = $('<span>', { 'class': 'poty-vote-button-text', text: setLabel });
		
		if (poty.isVotingPage) {
			var fileName   = poty.getFileNameFromPageName(),
				label      = poty.data[poty.galleryType].votes[fileName] ? poty.getMessage('vote-remove') : poty.getMessage('vote-add'),
				iconClass  = poty.data[poty.galleryType].votes[fileName] ? 'poty-ui-icon-vote-minus' : 'poty-ui-icon-vote-plus',
				$container = $('#potyEasyVoteEnhancedButton'),
				$vbt = $voteButtonText.clone().text(label),
				$votingButton = $('<button>', { title: label })
					.prepend($vbt)
					.prepend($('<span>', { 'class': iconClass + ' poty-icon md-inline-icon' }))
					.button({ disabled: poty.state !== poty.galleryType || !poty.data.eligible })
					.appendTo($container).data('potyImgName', fileName).click(poty.voteThroughVotingpage);
				
			poty.buttons.$button = poty.buttons.$button.add($votingButton);
			poty.buttons.$text = poty.buttons.$text.add($vbt);
		} else {
			poty.secureCall( 'galleryButtonRenderer' , statsLabel, setLabel, $voteButtonText, $statusButtonText, $setButtonText );
		}
		poty.secureCall('maybeDisableButtons');
		poty.nextTask();
	},
	detachGallery: function() {
		var h = poty.$gallery.height();
		poty.$gallery.$placeholder = $('<div>').text('Doing sophisticated magic!').height(h).appendTo(poty.$gallery.$oldParent);
		poty.$gallery.detach();
		poty.nextTask();
	},
	attachGallery: function() {
		poty.$gallery.$placeholder.remove();
		poty.$gallery.appendTo(poty.$gallery.$oldParent);
		poty.nextTask();
	},
	// seeded "randomization" based on the user-name
	shuffleElements: function($parants, childSelector) {
		// Initialize the generator with the user-name
		Math.seedrandom(poty.username);
		
		// Detaching from DOM to speed-up large element-shuffle
		// if we would just know $li.length ...
		$parants.each(function (i, parent) {
			var $par = $(parent),
				$children = $par.children(childSelector);
				
			while ($children.length) {
				$par.append($children.splice(Math.floor(Math.seededrandom() * $children.length), 1)[0]);
			}
		});
	},
	shuffleGallery: function() {
		poty.secureCall('shuffleElements', poty.$gallery.detach(), poty.listSelector);
		if (!poty.willAttachLater) poty.$gallery.appendTo(poty.$gallery.$oldParent);
		poty.nextTask();
	},
	checkLanguage: function() {
		// Only change the language automatically for new users who haven't set their language yet
		var browserLang = navigator.userLanguage || navigator.language || navigator.browserLanguage;
		if (!poty.dataExist && poty.data.local.editcount < 5 && browserLang !== poty.userlanguage && 'en' === poty.userlanguage && browserLang in poty.availableLanguages) {
			poty.secureCall('changeLangTo', browserLang);
		} else {
			poty.nextTask();
		}
	},
	
	/********************************
	**
	** My POTY
	**
	********************************/
	installMyPOTY: function() {
		// On the control-panel page, there should be no myPOTY button
		if (!poty.$myPotyPanelContainer.length) {
			var $p = mw.util.addPortletLink('p-personal', '#', poty.getMessage('my-poty-link'), 'pt-poty', poty.getMessage('my-poty-link-tooltip'), '', document.getElementById('pt-logout'));
			if ($p) {
				$p = $($p);
				$p.click(poty.myPOTY);
			}
			$('#com-my-poty-button').click(poty.myPOTY);
		}
		poty.nextTask();
	},
	changeLangTo: function(lcId, cb) {
		mw.libs.settingsManager.switchPref( 'language', lcId, function() {
			if ($.isFunction(cb)) {
				cb();
			} else {
				poty.userlanguage = lcId;
				poty.secureCall('loadTranslation');
			}
		} );
	},
	myPOTYPanelPage: function() {
		document.title = $('h1').not('#firstHeading').text() + ' - ' + poty.getMessage('poty-full-commons');
		poty.tasks = [];
		
		poty.addTask('showSplash');
		poty.addTask('loadComponents');
		poty.addTask('launch');
		poty.nextTask();
	},
	showMyPOTYPanelPage: function() {
		poty.$myPotyPanelContainer.empty().append(poty.$myPOTY());
		poty.nextTask();
	},
	myPOTY: function(e) {
		var $d = poty.$myPOTY(e);
		$d.dialog({
			title: poty.getMessage('my-poty-h1'),
			modal: true,
			height: 'auto',
			width: Math.min($(window).width(), 700),
			close: function() {
				$(this).remove();
			}
		});
	},
	$myPOTY: function(e) {
		if (e) e.preventDefault();
		var $d = $('<div>').attr('id', 'poty-mypoty').css({ 'max-width': 800 });
		$('<div>', { css: { 'float': 'right' } }).append($('<img>', { src: poty.logo, css: { width: '80px', display: 'none' } })).appendTo($d);
		$('<div>', { css: { 'font-size': 'smaller' }, text: poty.getMessage('poty-full-commons') + '; ' + poty.getMessage('my-poty-app-version', poty.appName, poty.version) }).appendTo($d);
		var $l = $('<div>', { id: 'myPotyLang' }).append($('<h3>', { text: poty.getMessage('my-poty-language') })).appendTo($d);
		var $s = $('<div>', { id: 'myPotyState' }).append($('<h3>', { text: poty.getMessage('my-poty-state') })).appendTo($d);
		var $e = $('<div>', { id: 'myPotyEligibility' }).append($('<h3>', { text: poty.getMessage('my-poty-eligibility') })).appendTo($d);
		var $v = $('<div>', { id: 'myPotyVotes' }).append($('<h3>', { text: poty.getMessage('my-poty-votes') })).appendTo($d);
		var $a = $('<div>', { id: 'myPotyData' }).append($('<h3>', { text: poty.getMessage('my-poty-data') })).appendTo($d);
		
		// Language
		var $ls;
		$('<button>', { text: poty.getMessage('my-poty-action-language-saveoncommons'), style: 'float:right;' })
			.button({ icons: { primary: 'ui-icon-disk' }}).click(function() {
				var $btn = $(this);
				$btn.unbind('click');
				var _done = function() {
					$btn.unblock();
					poty.reloadPage();
				};
				$btn.block({ message: $.createSpinner() });
				poty.changeLangTo($ls.val(), _done);
			}).appendTo($l);
		
		$ls = $('<select>', { size: 1 }).appendTo($l);
		$.each(poty.availableLanguages, function(s, l) {
			$ls.append($('<option>', { 'value': s, text: l }));
		});
		$ls.val(poty.userlanguage);
		
		// State
		var statetext;
		if (/R\d/.test(poty.state)) {
			statetext = poty.getMessage('my-poty-state-RX', poty.state.slice(1));
		} else {
			statetext = poty.getMessage('my-poty-state-novote');
		}
		$('<p>', { text: statetext + ' ' + poty.getMessage('my-poty-state-g-RX', poty.galleryType.slice(1)) }).appendTo($s);
		
		// Eligibility
		$('<button>', { text: poty.getMessage('my-poty-action-eligibility-recheck'), style: 'float:right;' })
			.button({ icons: { primary: 'ui-icon-search' }}).click(function() {
				delete poty.data.ineligible;
				delete poty.data.eligible;
				poty.saveData();
				poty.reloadPage();
			}).appendTo($e);
		if (poty.data.eligible) {
			$('<p>', { text: poty.getMessage('eligible', poty.data.eligible.edits, poty.data.eligible.on.name, poty.getDateFromMWDate(poty.required.editsBefore).toLocaleString()) }).appendTo($e);
		} else {
			var reason = firstItemName(poty.data.ineligible),
				item = firstItem(poty.data.ineligible),
				args = ['ineligible-' + reason];
			/*jshint onecase:true*/
			switch (reason) {
				case 'blocked': 
					args.push(item.by, item.reason, item.exp);
					break;
				default:
					args.push(poty.required.minEditCount, poty.getDateFromMWDate(poty.required.editsBefore).toLocaleString());
					break;
			}
			$('<p>', { text: poty.getMessage.apply(this, args) }).appendTo($e);
		}
		
		// Votes
		$('<button>', { text: poty.getMessage('my-poty-action-votes-recheck'), style: 'float:right;' })
			.button({ icons: { primary: 'ui-icon-search' }}).click(function() {
				var $btn = $(this);
				$btn.unbind('click');
				$btn.block({ message: $.createSpinner() });
				
				// TODO: Find a cleaner way
				poty.tasks = [];
				poty.addTask(function() {
					try {
						if (poty.$myPotyPanelContainer.length) {
							poty.showMyPOTYPanelPage();
						} else {
							$d.dialog('close');
							poty.myPOTY();
						}
					} catch(ex) {
						poty.reloadPage();
					}
				});
				poty.contribsDigger();
			}).appendTo($v);
		var listVotes = function(r) {
			$('<h4>', { text: r }).appendTo($v);
			var $vl = $('<ul>', { css: { 'max-height': '100px', 'margin-top': '2em', 'overflow': 'auto' } }).appendTo($v);
			$.each(poty.data[r].votes, function(f, bool) {
				if (bool) $('<li>').append($('<a>', { href: mw.util.getUrl('File:' + f), text: f }), ' ', $('<a>', { href: poty.getVotingPageURL(f, r), text: '(vote page)' })).appendTo($vl);
			});
		};
		poty.forEachRound(listVotes);
		
		// Data
		var $dd = $('<div>', { css: { 'max-height': '75px', overflow: 'auto', background: '#fff', border: '1px dotted gray' }, text: JSON.stringify(poty.data) }).appendTo($a);
		poty.createNotifyArea($('<span>', { text: poty.getMessage('my-poty-action-data-remove-warn', poty.appName) }), 'ui-icon-alert', 'ui-state-highlight').appendTo($a);
		$('<button>', { text: poty.getMessage('my-poty-action-data-remove') }).appendTo($a).button({ icons: { primary: 'ui-icon-trash' }}).click(function() {
			poty.deleteData();
			poty.data = poty.getData();
			$dd.text(JSON.stringify(poty.data));
		});
		
		return $d;
	},
	// Contibs digger
	diggerRunning: false,
	digMap: {},
	uccontinue: '',
	contribsDigger: function() {
		if (poty.diggerRunning) poty.log('abort new instance: digger is running');
		poty.diggerRunning = true;
		poty.uccontinue = '';
		poty.forEachRound(function(r) {
			poty.data[r].votes = {};
		});
		poty.secureCall('digContribs');
	},
	digContribs: function() {
		var query = {
			action: 'query',
			rawcontinue: '',
			list: 'usercontribs',
			uclimit: 'max',
			ucend: poty.startDate,
			ucuser: poty.username,
			ucnamespace: poty.pageNamespace,
			ucprop: 'title'
		};
		if (poty.uccontinue) query.uccontinue = poty.uccontinue;
		poty.queryAPI(query, 'digContribsCb', undefined, 'POST');
	},
	digContribsCb: function(r) {
		var uc = r.query.usercontribs;
		if (0 === uc.length) return poty.digVotingPages();
		$.each(uc, function(i, c){
			if (poty.votingPageRegExp.test(c.title)) poty.digMap[c.title] = true;
		});
		if (r['query-continue']) {
			poty.uccontinue = r['query-continue'].usercontribs.ucstart;
			poty.digContribs();
		} else {
			return poty.secureCall('digVotingPages');
		}
	},
	digVotingPages: function() {
		var toQuery = [];
		$.each(poty.digMap, function(p, b) {
			if (b) {
				toQuery.push(p);
				poty.digMap[p] = false;
				if (toQuery.length > 20) return false;
			}
		});
		if (toQuery.length) {
			poty.queryAPI({
				action: 'query',
				prop: 'revisions',
				rvprop: 'content',
				redirects: true,
				titles: toQuery.join('|')
			}, 'digVotingPagesCb', undefined, 'POST');
		} else {
			poty.diggerRunning = false;
			poty.saveData();
			poty.nextTask();
		}
	},
	digVotingPagesCb: function(r) {
		var pgs = r.query.pages;
		$.each(pgs, function(ids, p) {
			// Page moved, deleted, whatever that should never happen in usercontribs ...
			if (!p.revisions) return;
			var content = p.revisions[0]['*'];
			var title = p.title;
			poty.voteRegExp.lastIndex = 0;
			if (poty.voteRegExp.test(content)) {
				var r, t, m = title.match(poty.votingPageRegExp);
				r = m[1];
				t = m[2].replace('File:', '');
				poty.data[r].votes[t] = true;
			}
		});
		poty.digVotingPages();
	},
	
	/********************************
	**
	** Statistics
	**
	********************************/
	showStatsOrInfo: function() {
		if (poty.useStats) {
			poty.showStats(this);
		} else {
			poty.showInfo(this);
		}
	},
	getInfo: function(fn, cb) {
		poty.infocache = poty.infocache || {};
		var inf = poty.infocache[fn];
		if (inf) return cb(inf);
		if (poty.infoFromFileDescriptionPage) {
			poty.queryAPI({
				action: 'query',
				prop: 'imageinfo',
				iilimit: 1,
				iiprop: 'timestamp|user|url|extmetadata',
				iiextmetadatalanguage: poty.userlanguage,
				redirects: 1,
				titles: 'File:' + fn
			}, function(r) {
				var pg = firstItem(r.query.pages);
				if (!pg) return cb("- File not found -", true);
				
				var ii = pg.imageinfo[0];
				if (!ii) return cb("- No imageinfo for that file -", true);
				
				var imgDesc = ii.extmetadata.ImageDescription;
				if (!imgDesc) return cb("- File has no machine-readable description -", true);
				
				// Strip any markup
				var desc = $('<a>').html(imgDesc.value).text(),
					date = (ii.extmetadata.DateTime || {}).value,
					$editLink = $('<a>')
						.attr('href', mw.util.wikiScript() + '?' + $.param({
							action: 'edit',
							title: 'File:' + fn
						}) + '#editform')
						.attr('title', "Edit description")
						.append(poty.mdCreatejIcon('ui-icon-pencil'));
				
				if (date) date = '<hr />' + date;
				return cb(desc + ' ' + $('<span>').append($editLink).html() + date, true);
			});
		} else {
			$.get(mw.util.wikiScript() + '?' + $.param({
				'action': 'render',
				'title': poty.getInfoPage(fn)
			})).done(function(r) {
				if (r) poty.infocache[fn] = r;
				cb(r, true);
			}).fail(function() {
				cb("- No information available -", true);
			});
		}
	},
	showInfo: function(b) {
		var $b			= $(b),
			$gb			= $b.closest(poty.listSelector),
			fileName	= $b.data('potyImgName'),
			$info		= $('<div>').css('min-height', '3em').text("Loading info"),
			w, $blocked,
			displayInfo, to;
			
		displayInfo = function(i, repos) {
			var newHeight, oldHeight,
				parentHeight, newTop;
				
			if ($blocked) oldHeight = $blocked.height();
			$info.html(i);
			if (!repos || !$blocked) return;
			
			newHeight = $blocked.height();
			$info.fadeTo(0, 0).fadeTo('fast', 1);
			$blocked.css('overflow', 'hidden').height(oldHeight);
			
			// Compute center position
			parentHeight = $blocked.closest(poty.listSelector).height();
			newTop = (parentHeight - newHeight)/2;
			$blocked.animate({
				top: newTop,
				height: newHeight
			}, 'fast');
		};
			
		poty.secureCall('getInfo', fileName, displayInfo);
			
		w = $gb.width();
		w = Math.min(w < 350 ? Math.round((400/w)*40) : 30, 90);
		$gb.block({ 
			message: $('<div>', { style: 'font-size:.8em; line-height:1.1em' })
				.append($info)
				.append('<hr/>')
				.append($('<a>', { href: poty.getVotingPageURL(fileName), text: poty.getMessage('stats-votelist'), title: poty.getMessage('stats-votelist') }))
				/*.append(' &bull; ')
				.append($('<a>', { href: poty.getCommentsPageURL(fileName), text: poty.getMessage('stats-comments'), title: poty.getMessage('stats-comments') }))*/,
			css: {
				width: w + '%',
				'border-radius': '5px',
				'border': '1px solid #ABE'
			}
		});
		var startTimer = function() {
			clearTimeout(to);
			to = setTimeout(function() {
				$gb.unblock();
			}, 10000);
		};
		$blocked = $gb.find('.blockUI').css({'cursor': 'default'})
			.attr('title', poty.getMessage('stats-close-click')).click(function() { 
				$gb.unblock();
				clearTimeout(to);
			}).hover(function() {
				clearTimeout(to);
			}, function() {
				startTimer();
			}).filter('.blockMsg');
		startTimer();
	},
	showStats: function(b) {
		var $b			= $(b),
			$gb			= $b.closest(poty.listSelector),
			fileName	= $b.data('potyImgName'),
			$chart		= $('<div>').text('Loading chart');
			
		// Incompatible browsers
		var incompat = {
			'camino': 2 // Blacklisted due to a report by [[User:Kersti Nebelsiek]] on [[Special:Permalink/72839941#Results?]] (was version 1.6 but let's be sure)
		};
		var clnt = $.client.profile();
		
		if ((!isCanvasSupported() && 'msie' !== clnt.name) || ((clnt.name in incompat) && (incompat[clnt.name] >= clnt.versionNumber))) {
			window.location = poty.getVotingPageURL(fileName);
			return;
		}

		poty.secureCall('loadStats', function() {
			mw.loader.using('ext.gadget.jquery.sparkline', function() {
				poty.secureCall('showStatsCb', $chart, $gb, fileName);
			});
		});
		
		var w = $gb.width();
		w = Math.min(w < 350 ? Math.round((400/w)*40) : 30, 90);
		$gb.block({ 
			message: $('<div>', { style: 'font-size:smaller' })
				.text(poty.getMessage('stats-chart-desc'))
				.append($chart)
				.append($('<a>', { href: poty.getVotingPageURL(fileName), text: poty.getMessage('stats-votelist') })),
			css: {
				width: w + '%'
			}
		});
		$gb.find('.blockUI').css({'cursor': 'default'}).attr('title', poty.getMessage('stats-close-click')).click(function() { $gb.unblock(); });
		setTimeout(function() {
			$gb.unblock();
		}, 10000);
	},
	showStatsCb: function($chart, $gb, fileName) {
		var diffAvgVots = [],
			imageVots = [],
			maxVoteCount = 0,
			maxDiff = 0;

		imageVots = poty.statistics[fileName];
		$.each(imageVots, function(i, votecount) {
			var totalVotes = 0,
				voteCont = 0;
			$.each(poty.statistics, function(fName, votes) {
				if ('number' === typeof votes[i]) {
					if (votes[i] > maxVoteCount) maxVoteCount = votes[i];
					totalVotes += votes[i];
					voteCont++;
				}
			});
			var diff = votecount-Math.round(totalVotes/voteCont);
			if (Math.abs(diff) > maxDiff) maxDiff = Math.abs(diff);
			diffAvgVots.push(diff);
		});
		// height: diff of max equals 60px
		var h = Math.round(maxDiff*60/maxVoteCount) + 5;
		setTimeout(function() {
			$chart.sparkline(diffAvgVots, { height: h + 'px', width: '80%', type: 'bar', fillColor: false });
		}, 500);
	},
	genStats: function() {
		poty.genericVoteRegExp = new RegExp(poty.mdEscapeSpecial($.escapeRE(poty.votingFormat)).replace(/%UserName%/g, '[^\\|\\[\\]]+'), 'g');
		mw.loader.using([], function() {
			poty.gapfrom = '';
			poty.secureCall('loadStats', poty.updateStats);
		});
	},
	loadStats: function(cb) {
		if (poty.statistics) return cb();
		$.ajax({
			url: mw.util.wikiScript(),
			dataType: 'json',
			data: {
				title: poty.getStatsPage(),
				action: 'raw',
				ctype: 'text/javascript',
				// Disallow caching
				maxage: 0,
				smaxage: 0
			},
			cache: false,
			success: function(r) {
				poty.statistics = r || [];
				$.each(poty.statistics, function(i, d) {
					d[0] = new Date(d[0]);
				});
				cb();
			}
		});
	},
	updateStats: function() {
		poty.queryAPI({
			action: 'query',
			rawcontinue: '',
			generator: 'allpages',
			gapnamespace: poty.pageNamespace,
			gapfilterredir: 'nonredirects',
			gaplimit: 100,
			gapprefix: poty.pageBase + '/' + poty.year + '/' + poty.galleryType + '/v/',
			gapfrom: poty.gapfrom,
			prop: 'revisions',
			rvprop: 'content'
		}, 'updateStatsCB', undefined, 'POST');
	},
	updateStatsCB: function(r) {
		var pgs = r.query.pages;
		$.each(pgs, function(ids, pg) {
			var c = pg.revisions[0]['*'],
				t = pg.title,
				f = poty.getFileNameFromPageName(t),
				m = c.match(poty.genericVoteRegExp),
				l = 0;

			if (!f) return;
			if (!m) return;
			$.each( m, function( i, u ) {
				if ( u in poty.voters ) {
					return;
				}
				poty.voters[u] = 1;
				l++;
			} );
			poty.totalVoters += l;
		});
		if (!r['query-continue']) {
			poty.statistics.push([ new Date(), poty.totalVoters ]);
			mw.libs.commons.api.editPage({
				editType: 'text',
				title: poty.getStatsPage(),
				text: JSON.stringify(poty.statistics),
				summary: 'updating voting statistics',
				recreate: false,
				minor: true,
				watchlist: 'nochange'
			});
		} else {
			poty.gapfrom = r['query-continue'].allpages.gapcontinue;
			poty.secureCall('updateStats');
		}
	},
	/*
	* Loads heavy statistics plugIn
	*/
	advancedStats: function() {
		mw.loader.using(['ext.gadget.jquery.jqplot']).then(function() {
			poty.secureCall('loadStats', poty.showAdvancedStats);
		});
	},
	showAdvancedStats: function() {
		var $plot = $('#votesByDate').text('');
		$plot.jqplot([poty.statistics], {
			title: "Voters versus time",
			axesDefaults: {
				labelRenderer: $.jqplot.CanvasAxisLabelRenderer
			},
			gridPadding: { right: 35 },
			axes:{
				xaxis:{
					renderer: $.jqplot.DateAxisRenderer,
					label: "Time"
				},
				yaxis:{
					label: "Voters",
					pad: 0
				}
			},
			series:[{ lineWidth: 1 }],
			highlighter: {
				show: true,
				sizeAdjust: 7.5
			},
			cursor: {
				show: true,
				zoom:true,
				showTooltip: false
			}
		});
		
		// poty.statistics
		// poty.statisticsDates
	},
	
	/********************************
	**
	** Voting
	**
	********************************/
	lock: function() {
		poty.locked = true;
	},
	unlock: function() {
		poty.locked = false;
	},
	voteXWindowListener: function() {
		poty.nextTask();
		var oldCount;
		
		// Will be executed when there is time to
		var __handleStorageChange = function() {
			$.jStorage.reInit();
			poty.data = poty.getData();
			var newCount = poty.getCurrentVoteCount();
			if (oldCount !== newCount) {
				oldCount = newCount;
				if (poty.isVotingPage) {
					poty.reloadPage();
				} else {
					// Insert a dummy function
					poty.addTask($.noop);
					// Recalc buttons
					poty.setUpButtons();
				}
			}
		};
		var t = 0;
		var _perhapsHandleStorageChange = function(key, action) {
			clearTimeout(t);
			if (poty.locked) return;
			t = setTimeout(__handleStorageChange, 750);
		};
		oldCount = poty.getCurrentVoteCount();
		
		$.jStorage.listenKeyChange(poty.storageKey, _perhapsHandleStorageChange);
	},
	voteBlockImg: function($el, msg) {
		if (!isNumber(poty.maxVotesR) || poty.maxVotesR > 5) {
			var w = $el.width();
			w = Math.min(w < 350 ? Math.round((400/w)*40) : 30, 90);
			$el.block({ 
				message: poty.getMessage(msg, poty.maxVotesR),
				css: {
					width: w + '%'
				}
			});
		} else {
			poty.secureCall('showLoader');
			poty.secureCall('showImmediateBannerMessage', [msg]);
		}
	},
	voteUnblockImg: function($el) {
		if (!isNumber(poty.maxVotesR) || poty.maxVotesR > 5) {
			setTimeout(function () {
				$el.unblock();
			}, 1000);
		} else {
			poty.secureCall('hideLoader');
		}
	},
	voteMessageAndUnblock: function($el, msg) {
		if (!isNumber(poty.maxVotesR) || poty.maxVotesR > 5) {
			poty.voteBlockImg($el, msg);
			setTimeout(function () {
				poty.voteUnblockImg($el);
			}, 2000);
		} else {
			setTimeout(function () {
				poty.secureCall('hideLoader');
			}, 2000);
			poty.secureCall('showImmediateBannerMessage', [msg, poty.maxVotesR]);
		}
	},
	voteSetButtonPlus: function($b) {
		$b.find('.poty-ui-icon-vote-minus').removeClass('poty-ui-icon-vote-minus').addClass('poty-ui-icon-vote-plus');
		var addMsg = poty.getMessage('vote-add');
		$b.attr('title', addMsg).find('.poty-vote-button-text').text(addMsg);
	},
	voteSetButtonMinus: function($b) {
		$b.find('.poty-ui-icon-vote-plus').removeClass('poty-ui-icon-vote-plus').addClass('poty-ui-icon-vote-minus');
		var rmMsg = poty.getMessage('vote-remove');
		$b.attr('title', rmMsg).find('.poty-vote-button-text').text(rmMsg);
	},
	voteThroughGallery: function() {
		var $b          = $(this),
			fileName    = $b.data('potyImgName'),
			ii          = poty.images[fileName],
			$galleryBox = ii.$gb;

		poty.secureCall('vote', fileName, $galleryBox, $b);
	},
	voteThroughVotingpage: function() {
		var $b = $(this),
			fileName = poty.getFileNameFromPageName();
		poty.secureCall('vote', fileName, $b.parent(), $b, poty.reloadPage);
	},
	getVoteCount: function(votelist) {
		var c = 0;
		votelist = votelist || {};

		$.each(votelist, function(i, b) {
			if (b) c++;
		});
		return c;
	},
	getCurrentVoteCount: function() {
		return poty.getVoteCount((poty.data[poty.galleryType] || {}).votes);
	},
	vote: function(fileName, $toBlock, $b, readyCb) {
		var votingPage     = poty.getVotingPage(fileName),
			addText        = poty.addVoteTemplate || poty.formattedVote,
			removeRegExp   = poty.voteRegExp,
			basetimestamp  = '',
			starttimestamp = '',
			wikitext       = '',
			remove         = poty.data[poty.galleryType].votes[fileName];
		
		// Prevent reloading and other interferring actions
		// could be also delegated to other tabs/windows so
		// they could be locked
		poty.lock();
		
		if (remove) {
			poty.voteBlockImg($toBlock, 'voting-remove-vote');
		} else {
			poty.voteBlockImg($toBlock, 'voting-vote');
		}
		var _goToVotingPage = function() {
			window.location = poty.getVotingPageURL(fileName);
		};
		var __addVoteOk = function(r) {
			poty.voteSetButtonMinus($b);
			if ('~' === poty.maxVotesR && 0 === poty.getCurrentVoteCount()) {
				poty.voteMessageAndUnblock($toBlock, 'vote-multiple-possible');
			} else if (isNumber(poty.maxVotesR)) {
				poty.voteMessageAndUnblock($toBlock, 'vote-limited');
				if (poty.getCurrentVoteCount() + 1 >= poty.maxVotesR) {
					// Enforcing x vote count
					$('.poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', true);
				}
			} else {
				poty.voteUnblockImg($toBlock);
			}
			poty.data[poty.galleryType].votes[fileName] = true;
			if ('undefined' !== typeof r.edit['new']) {
				poty.autoreport('++created ' + votingPage);
			}
			poty.saveData();
			poty.unlock();
			if ($.isFunction(readyCb)) readyCb();
		};
		var __addVoteErr = function(t, r, q) {
			poty.voteMessageAndUnblock($toBlock, 'voting-edit-error');
			poty.fail('voteadd ' + votingPage + '; ' + t, _goToVotingPage);
			poty.unlock();
		};
		var _addVote = function() {
			mw.libs.commons.api.editPage({
				cb: __addVoteOk,
				errCb: __addVoteErr,
				editType: 'appendtext',
				title: votingPage,
				text: addText,
				summary: poty.votingSummaryAdd
					.replace('%wiki%', poty.data.eligible.on.name)
					.replace('%edits%', poty.data.eligible.edits)
					.replace('%VoteFrom%', mw.config.get('wgPageName').replace(/_/g, ' ')),
				recreate: false,
				minor: true,
				redirect: true,
				watchlist: 'nochange'
			});
		};
		var __removeVoteOk = function(r) {
			poty.voteSetButtonPlus($b);
			poty.voteUnblockImg($toBlock);
			poty.data[poty.galleryType].votes[fileName] = false;
			poty.saveData();
			if (isNumber(poty.maxVotesR) && poty.getCurrentVoteCount() < poty.maxVotesR) {
				// Enforcing x vote count
				$('.poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', false);
			}
			poty.unlock();
			if ($.isFunction(readyCb)) readyCb();
		};
		var __removeVoteErr = function(t, r, q) {
			poty.voteMessageAndUnblock($toBlock, 'voting-edit-error');
			poty.fail('voteremove ' + votingPage + '; ' + t, _goToVotingPage);
			poty.unlock();
		};
		var _removeVote = function() {
			var newText = wikitext.replace(removeRegExp, '');
			newText = newText || ' ';
			mw.libs.commons.api.editPage({
				cb: __removeVoteOk,
				errCb: __removeVoteErr,
				editType: 'text',
				title: votingPage,
				text: newText,
				summary: poty.votingSummaryRemove
					.replace('%VoteFrom%', mw.config.get('wgPageName').replace(/_/g, ' ')),
				recreate: false,
				minor: true,
				redirect: true,
				watchlist: 'nochange'
			});
		};
		var _gotWikitext = function(r) {
			try {
				var p = firstItem(r.query.pages),
					rv;
					
				if (p.revisions) {
					rv = p.revisions[0];
					starttimestamp = p.starttimestamp;
					basetimestamp = r.timestamp;	
					wikitext = rv['*'];
				} else {
					wikitext = '';
				}
				
				// Cave:
				// http://stackoverflow.com/questions/1520800/why-regexp-with-global-flag-in-javascript-give-wrong-results
				removeRegExp.lastIndex = 0;
				var contains = removeRegExp.test(wikitext);

				if (remove) {
					if (contains) {
						_removeVote();
					} else {
						poty.voteMessageAndUnblock($toBlock, 'vote-nothing-to-remove');
						poty.voteSetButtonPlus($b);
						poty.data[poty.galleryType].votes[fileName] = false;
						poty.unlock();
					}
				} else {
					if (contains) {
						poty.voteMessageAndUnblock($toBlock, 'vote-already-there');
						poty.voteSetButtonMinus($b);
						poty.data[poty.galleryType].votes[fileName] = true;
						poty.unlock();
					} else {
						_addVote();
					}
				}
			} catch (ex) {
				poty.voteMessageAndUnblock($toBlock, 'voting-app-error');
				poty.fail(ex + ' at ' + votingPage, _goToVotingPage);
			}
			poty.saveData();
		};
		poty.queryAPI({
			action: 'query',
			prop: 'info|revisions',
			intoken: 'edit',
			rvprop: 'timestamp|content',
			rvlimit: 1,
			titles: votingPage,
			redirects: 1
		}, _gotWikitext, null, 'POST');
		
		poty.saveData();
	},
	
	
	/********************************
	**
	** Eligiblity check
	**
	********************************/
	showIneligible: function () {
		poty.saveData();
		poty.log('ineligible');
		
		poty.secureCall('continueEligible');
	},
	showEligible: function () {
		poty.saveData();
		poty.log('Eligible!');
		
		poty.secureCall('continueEligible');
	},
	continueEligible: function () {
		// Just task scheduling
		poty.tasks = [];
		
		if (!poty.isVotingPage) {
			poty.addTask('detachGallery');
			poty.addTask('setUpGallery');
		}
		poty.addTask('installMyPOTY');
		if (!poty.isVotingPage) poty.addTask('installSlideshow');
		if (!poty.dataExist && poty.data.local.editcount > 0) poty.addTask('contribsDigger');
		poty.addTask('setUpButtons');
		if (poty.maxVotesR !== '~') poty.addTask('voteXWindowListener');
		if (!poty.isVotingPage) {
			poty.addTask('shuffleGallery');
			poty.willAttachLater = true;
			poty.addTask('attachGallery');
		}
		if (poty.$myPotyPanelContainer.length) {
			poty.addTask('showMyPOTYPanelPage');
		}
		poty.addTask('hideLoader');
		poty.nextTask();
	},
	checkLocal: function () {
		poty.queryAPI({
			action: 'query',
			meta: 'userinfo',
			uiprop: 'blockinfo|ratelimits|editcount|registrationdate|preferencestoken'
		}, 'checkLocalCb');
	},
	checkLocalCb: function (r) {
		poty.log('userinfo', r);
		var ui = r.query.userinfo;
		poty.username = window.debugPOTYUserName || ui.name;
		if (ui.ratelimits.ip) {
			var l = ui.ratelimits.edit.ip;
			if (l) {
				poty.ratelimit = Math.floor(l.hits / (l.seconds / 60));
			}
		}
		if (ui.blockexpiry) {
			poty.data.ineligible = {
				blocked: {
					by: ui.blockedby,
					reason: ui.blockreason,
					exp: ui.blockexpiry
				}
			};
			return poty.secureCall('showIneligible');
		} else {
			if (poty.data.ineligible && poty.data.ineligible.blocked) poty.data.ineligible = null;
		}
		if (poty.data.local.editcount && poty.data.local.id !== ui.id) {
			poty.log('Wrong user data. Ereasing...');
			poty.deleteData();
			poty.data = {};
			poty.saveData();
			return poty.secureCall('relaunch');
		}
		poty.data.local = {
			id: ui.id,
			editcount: ui.editcount,
			registrationdate: ui.registrationdate
		};
		poty.preferencestoken = ui.preferencestoken;
		poty.nextTask();
		if ('string' === typeof ui.anon && !mw.user.isAnon()) {
			return poty.showAnonWarning();
		}
	},
	checkSUL: function () {
		poty.queryAPI({
			action: 'query',
			meta: 'globaluserinfo',
			guiprop: 'merged',
			guiuser: poty.username
		}, 'checkSULCb');
	},
	checkSULCb: function (r) {
		var sul = r.query.globaluserinfo,
			sulmap = {},
			sularr = [];
			
		if (typeof sul.missing !== 'undefined') {
			poty.data.sulmissing = true;
			return poty.nextTask();
		}
		poty.data.sul = {
			creationTime: sul.registration,
			id: sul.id
		};
		$.each(sul.merged, function (i, account) {
			// Map the number of edits to wikis and also add the edit-numbers to an array
			if (account.wiki === poty.wiki) return;
			if (!sulmap[account.editcount]) sulmap[account.editcount] = [];
			sulmap[account.editcount].push(account);
			sularr.push(account.editcount - 0);
		});
		var numsort = function (n1, n2) {
				return n1 - n2;
			};
		sularr.sort(numsort);
		poty.sulInfo = [];
		var seenEditCount = -1;
		var i = sularr.length - 1;
		for (; i !== -1; i--) {
			var editcount = sularr[i];
			if (editcount === seenEditCount) continue;
			if (editcount < poty.required.minEditCount) continue;
			poty.sulInfo.push(sulmap[editcount]);
			seenEditCount = editcount;
		}
		if (0 === poty.sulInfo.length && poty.data.local.editcount < poty.required.minEditCount) {
			poty.data.ineligible = {
				suleditcount: true
			};
			return poty.secureCall('showIneligible');
		}
		poty.nextTask();
	},
	getDbList: function () {
		poty.queryAPI({
			action: 'sitematrix'
		}, 'getDbListCb');
	},
	getDbListCb: function (r) {
		poty.sitematrix = {};
		$.each(r.sitematrix, function (i, sites) {
			if (!isNumber(i)) return;
			$.each(sites.site, function (x, s) {
				poty.sitematrix[s.dbname] = {
					api: s.url.replace('http://', '//') + '/w/api.php',
					langcode: sites.code,
					typecode: s.code
				};
			});
		});
		$.each(r.sitematrix.specials, function (i, s) {
			poty.sitematrix[s.dbname] = {
				api: s.url.replace('http://', document.location.protocol + '//') + '/w/api.php',
				specialcode: s.code
			};
		});
		poty.nextTask();
	},
	checkLocalExistingContribs: function () {
		poty.queryAPI({
			action: 'query',
			list: 'usercontribs',
			ucuser: poty.username,
			ucstart: poty.required.editsBefore,
			uclimit: poty.required.minEditCount,
			ucprop: '',
			requestid: poty.wiki
		}, 'checkLocalExistingContribsCb');
	},
	checkLocalExistingContribsCb: function (r) {
		var uc = r.query.usercontribs;
		if (uc.length < poty.required.minEditCount) {
			// Not enough edits
			return poty.nextTask();
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.length + '+'
		};
		poty.showEligible();
	},
	checkLocalContribs: function () {
		poty.queryAPI({
			action: 'userdailycontribs',
			user: poty.username,
			daysago: poty.required.editsDaysAgo,
			basetimestamp: poty.required.editsBefore,
			requestid: poty.wiki
		}, 'checkLocalContribsCb');
	},
	checkLocalContribsCb: function (r) {
		var uc = r.userdailycontribs;

		// First check the registration date
		if (parseInt(uc.registration, 10) > parseInt(poty.required.registeredBefore.replace(/\D/g, ''), 10)) {
			// Registered later
			return poty.nextTask();
		}
		if (parseInt(uc.timeFrameEdits, 10) < poty.required.minEditCount) {
			// Not enough edits
			return poty.nextTask();
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.timeFrameEdits,
			registration: uc.registration
		};
		poty.showEligible();
	},
	checkGlobalExistingContribs: function () {
		if (poty.data.sulmissing) {
			poty.data.ineligible = {
				nosul: true
			};
			return poty.secureCall('showIneligible');
		}
		// Next group of contribs-count
		poty.currentEditGroup++;
		if (!poty.sulInfo[poty.currentEditGroup]) {
			poty.data.ineligible = {
				dateeditcount: true
			};
			return poty.secureCall('showIneligible');
		}
		$.each(poty.sulInfo[poty.currentEditGroup], function (i, s) {
			if (!(s.wiki in poty.sitematrix)) throw new Error('There are contributions in CentralAuth for an unknown wiki.');
			var url = poty.sitematrix[s.wiki].api;
			poty.queryAPI({
				action: 'query',
				list: 'usercontribs',
				ucuser: poty.username,
				ucstart: poty.required.editsBefore,
				uclimit: poty.required.minEditCount,
				ucprop: '',
				requestid: s.wiki
			}, 'checkGlobalExistingContribsCb', url);
			poty.queriesRunning++;
		});
	},
	checkGlobalExistingContribsCb: function (r) {
		poty.queriesRunning--;
		var uc = r.query.usercontribs;
		if (poty.data.eligible) return;
		if (uc.length < poty.required.minEditCount) {
			// Not enough edits
			if (0 === poty.queriesRunning) poty.secureCall('checkGlobalExistingContribs');
			return;
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.length + '+'
		};
		poty.showEligible();
	},
	checkGlobalContribs: function () {
		if (poty.data.sulmissing) {
			poty.data.ineligible = {
				nosul: true
			};
			return poty.secureCall('showIneligible');
		}
		// Next group of contribs-count
		poty.currentEditGroup++;
		if (!poty.sulInfo[poty.currentEditGroup]) {
			poty.data.ineligible = {
				dateeditcount: true
			};
			return poty.secureCall('showIneligible');
		}
		$.each(poty.sulInfo[poty.currentEditGroup], function (i, s) {
			if (!(s.wiki in poty.sitematrix)) throw new Error('There are contributions in CentralAuth for an unknown wiki.');
			var url = poty.sitematrix[s.wiki].api;
			poty.queryAPI({
				action: 'userdailycontribs',
				user: poty.username,
				daysago: poty.required.editsDaysAgo,
				basetimestamp: poty.required.editsBefore,
				requestid: s.wiki
			}, 'checkGlobalContribsCb', url);
			poty.queriesRunning++;
		});
	},
	checkGlobalContribsCb: function (r) {
		poty.queriesRunning--;
		var uc = r.userdailycontribs;
		if (poty.data.eligible) return;
		// First check the registration date
		if (parseInt(uc.registration, 10) > parseInt(poty.required.registeredBefore.replace(/\D/g, ''), 10)) {
			// Registered later
			if (0 === poty.queriesRunning) poty.secureCall('checkGlobalContribs');
			return;
		}
		if (parseInt(uc.timeFrameEdits, 10) < poty.required.minEditCount) {
			// Not enough edits
			if (0 === poty.queriesRunning) poty.secureCall('checkGlobalContribs');
			return;
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.timeFrameEdits,
			registration: uc.registration
		};
		poty.showEligible();
	},
	loadTranslation: function() {
		var l = poty.userlanguage;
		switch (l) {
			case 'nb':
				l = 'no';
				break;
			case 'zh-hans':
			case 'zh-cn':
			case 'zh-sg':
			case 'zh-my':
				l = 'zh-hans';
				break;
			default:
				l = l.split('-')[0];
		}
		if (poty.userlanguage !== 'en') {
			poty.translationLoaded = true;
			$.ajax({
				url: poty.wikiScript,
				dataType: 'script',
				data: {
					title: 'User:Leaderboard/Gadget-EnhancedPOTY.js/i18n/' + l + '.js',
					action: 'raw',
					ctype: 'text/javascript',
					// Allow caching for 1/2 day
					maxage: 43200,
					smaxage: 43200
				},
				cache: true,
				success: poty.nextTask,
				error: poty.nextTask
			});
		} else {
			poty.nextTask();
		}
	},
	/**
	** Overwriting this method allows
	** changing translations after they have been loaded.
	**/
	loadTranslationDone: function() {
		poty.nextTask();
	},

	/**
	** Does a MediaWiki API request and passes the result to the supplied callback.
	**/
	queryAPI: function (params, callback, url, method) {
		mw.libs.commons.api.query(params,
		{
			cache: false,
			url: url,
			method: method,
			cb: function(r) {
				poty.secureCall(callback, r);
			},
			// r-result, query, text
			errCb: function(t, r, q) {
				poty.fail(t);
			}
		});
	},

	fail: function (err, cb) {
		poty.log('error', err);
		if (typeof err === 'object') {
			try {
				var stErr = err.message + ' \n\n ' + err.name;
				if (err.lineNumber) stErr += ' @line' + err.lineNumber;
				err = stErr;
			} catch (ex) {
				err = '';
			}
		}
		var $dlg = $('<div>'),
			dlgBtns = {};
			
		if (((poty.data.eligible && !poty.data.eligible.on) || -1 !== err.indexOf(' \'on\'')) && JSON.stringify) err = err + ' e:' + JSON.stringify(poty.data.eligible) + ';\n i:' + JSON.stringify(poty.data.ineligible);
		dlgBtns[poty.getMessage('report-error-send')] = function() {
			$dlg.parent().block();
			poty.autoreport(err, function() {
				poty.unlock();
				
				$dlg.dialog('close');
				$dlg.remove();
				if ($.isFunction(cb)) cb();
			});
		};
		dlgBtns[poty.getMessage('report-error-reset')] = function() {
			poty.unlock();
			// Delete saved data as this could be a problem
			poty.deleteData();
			// Purge the page and reloadPage
			poty.purgePage();
			$dlg.parent().block();
		};
		dlgBtns[poty.getMessage('report-error-cancel')] = function() {
			poty.unlock();
			$dlg.dialog('close');
		};
		try {
			poty.hideLoader();
		} catch (ex) {}
		poty.createNotifyArea($('<span>', { text: poty.getMessage('report-error', err) }), 'ui-icon-alert', 'ui-state-error').appendTo($dlg);
		$dlg.dialog({
			title: poty.getMessage('report-error-h1', poty.appName),
			buttons: dlgBtns,
			modal: true,
			height: 'auto',
			width: Math.min($(window).width(), 500),
			open: function() {
				// Look out for http://bugs.jqueryui.com/ticket/6830 / jQuery UI 1.9
				var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
				$buttons.eq(0).button({ icons: { primary: 'ui-icon-circle-check' } });
				$buttons.eq(1).button({ icons: { primary: 'ui-icon-wrench' } });
				$buttons.eq(2).button({ icons: { primary: 'ui-icon-circle-close' } });
			},
			close: function() {
				poty.hideLoader();
			}
		});
	},
	
	autoreport: function (errText, cb) {
		var randomId = Math.round(Math.random()*1099511627776);
		var currentTask = $.isFunction(poty.currentTask) ? (poty.currentTask.name ? poty.currentTask.name : 'inline') : poty.currentTask;
		var toSend = '\n== Autoreport by intelliVote ' + randomId + ' ==\n' + errText + 
			'\n++++\n:Task: ' + currentTask + '\n:NextTask: ' + poty.tasks[0] + '\n:LastTask: ' + poty.tasks[poty.tasks.length - 1] +
			'\n:Page: ' + (mw.config.get('wgPageName')) + '\n:Skin: ' + mw.user.options.get('skin') +
			'\n:[{{fullurl:Special:Contributions|target={{subst:urlencode:{{subst:REVISIONUSER}}}}&offset={{subst:REVISIONTIMESTAMP}}}} Contribs before error]';
		mw.libs.commons.api.editPage({
			cb: cb,
			errCb: cb,
			editType: 'appendtext',
			title: poty.reportPage,
			text: toSend,
			summary: '[[#Autoreport by intelliVote ' + randomId + '|Reporting a intelliVote-App error.]] Random ID=' + randomId,
			minor: true,
			watchlist: 'nochange'
		});
	},

	/**
	** Method to catch errors and report where they occurred
	**/
	secureCall: function (fn) {
		var o = poty;
		try {
			o.currentTask = arguments[0];
			if ($.isFunction(fn)) {
				if (fn.name) poty.log(fn);
				// arguments is not of type array so we can't just write arguments.slice
				return fn.apply(o, Array.prototype.slice.call(arguments, 1));
			} else if ('string' === typeof fn) {
				poty.log(fn);
				return o[fn].apply(o, Array.prototype.slice.call(arguments, 1));
			} else {
				poty.log('This is not a function!');
			}
		} catch (ex) {
			poty.log('failure at ' + fn);
			o.fail(ex);
		}
	},

	/**
	** Simple task queue.  addTask() adds a new task to the queue, nextTask() executes
	** the next scheduled task.  Tasks are specified as method names to call.
	**/
	tasks: [],
	// list of pending tasks
	currentTask: '',
	// current task, for error reporting
	oldTask: '',
	// task called before
	addTask: function (task) {
		poty.tasks.push(task);
	},
	nextTask: function () {
		var task = poty.currentTask = poty.tasks.shift();
		return ($.isArray(task) ? poty.secureCall.apply(poty, task) : poty.secureCall(task)); // Ja da guckste ...
	},
	lastTask: function () {
		var task = poty.currentTask = poty.tasks[poty.tasks.length - 1];
		poty.tasks = [];
		return ($.isArray(task) ? poty.secureCall.apply(poty, task) : poty.secureCall(task));
	},
	log: function (key, val) {
		if (window.console && $.isFunction(window.console.log)) window.console.log('POTY> ' + key, val/*, this*/);
	},
	reloadPage: function () {
		if (!poty.locked) window.location = window.location;
	},
	purgePage: function (page) {
		window.location = mw.util.wikiScript('index') + '?' + $.param({ title: mw.config.get('wgPageName') || page.replace(/ /g, '_'), action: 'purge' });
	}
};

// Now, merge configuration 
// (yes, you can use this script not only for POTY and not only on Commons)
window.potyconfig = window.potyconfig || {};
$.extend(true, poty, window.potyconfig);

// Stuff that should be overwritten and not extended.
var noExtend = ['availableImageSizes', 'availableImageSizesWide'];
$.each(noExtend, function(i, k) {
	if (k in window.potyconfig) poty[k] = window.potyconfig[k];
});

// And go!
poty.secureCall('init');
})(jQuery, mediaWiki);
// </nowiki>