Jump to content

User:Evad37/rater.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Evad37 (talk | contribs) at 06:29, 14 November 2017 (Version 0.3.1-alpha: fix fatal bug that occurred with talk page top sections not containing templates). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/***************************************************************************************************
 Rater --- by Evad37
 > Helps assess WikiProject banners.
 > Alpha version
***************************************************************************************************/
// <nowiki>
$( function($) {
/* ========== Config ============================================================================ */
// A global object that stores all the page and user configuration and settings
var config = {};
// Script info
config.script = {
	// Advert to append to edit summaries
	advert:  ' ([[User:Evad37/rater.js|Rater]])',
	version: '0.3.1-alpha'
};
// MediaWiki configuration values
config.mw = mw.config.get( [
	'wgPageName',
	'wgNamespaceNumber',
	'wgUserName',
	'wgFormattedNamespaces',
	'wgMonthNames',
	'wgRevisionId',
	'wgScriptPath',
	'wgServer'
] );
// Do not operate on Special: pages, nor on non-existent pages or their talk pages
if ( config.mw.wgNamespaceNumber < 0 || $('li.new[id|=ca-nstab]').length ) {
	return;
}
// For User and User_talk namespaces, only operate on subpages
if (
	config.mw.wgNamespaceNumber >= 2 &&
	config.mw.wgNamespaceNumber <= 3 &&
	config.mw.wgPageName.indexOf('/') === -1
) {
	return;
}
config.regex = {
	// Pattern to find templates, which may contain other templates
	template:		/\{\{\s*(.+?)\s*(\|(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?\}\})(?:.|\n)*?)*?\}\})(?:.|\n)*?)*|)\}\}\n?/g,
	// Pattern to find `|param=value` or `|value`, where `value` can only contain a pipe
	// if within square brackets (i.e. wikilinks) or braces (i.e. templates)
	templateParams:	/\|(?!(?:[^{]+}|[^\[]+]))(?:.|\s)*?(?=(?:\||$)(?!(?:[^{]+}|[^\[]+])))/g
};
config.deferred = {};
config.bannerDefaults = {
	classes: [
		'FA',
		'FL',
		'A',
		'GA',
		'B',
		'C',
		'Start',
		'Stub',
		'List'
	],
	importances: [
		'Top',
		'High',
		'Mid',
		'Low'
	],
	extendedClasses: [
		'Category',
		'Draft',
		'File',
		'Portal',
		'Project',
		'Template',
		'Bplus',
		'Future',
		'Current',
		'Disambig',
		'NA',
		'Redirect',
		'Book'
	],
	extendedImportances: [
		'Bottom',
		'NA'
	]
};
config.shellTemplates = [
	'WikiProject banner shell',
	'WikiProjectBanners',
	'WikiProject Banners',
	'WPB',
	'WPBS',
	'Wikiprojectbannershell',
	'WikiProject Banner Shell',
	'Wpb',
	'WPBannerShell',
	'Wpbs',
	'Wikiprojectbanners',
	'WP Banner Shell',
	'WP banner shell',
	'Bannershell',
	'Wikiproject banner shell',
	'WikiProject Banners Shell',
	'WikiProjectBanner Shell',
	'WikiProjectBannerShell',
	'WikiProject BannerShell',
	'WikiprojectBannerShell',
	'WikiProject banner shell/redirect',
	'WikiProject Shell',
	'Banner shell',
	'Scope shell',
	'Project shell'
];

/* ========== Load dependencies ================================================================= */
// Load Morebits gadget if not already available
if ( window.Morebits == null ) {
	importScript('MediaWiki:Gadget-morebits.js');
	importStylesheet( 'MediaWiki:Gadget-morebits.css' );
}
// Load extra.js if not already available
if ( window.extraJs == null ) {
	importScript('User:Evad37/extra.js');
}
// Load resoucre loader modules
mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'mediawiki.RegExp',
	'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows', 'jquery.ui.dialog'], function () {

/* ========== CSS =============================================================================== */
// TODO: convert to .css subpage and load using importStylesheet()
// Attribution: Diff styles from <https://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/style.css>
mw.util.addCSS(
	/* ----- Main dialog styles --------------------------------------------------------------- */
	'.rater-dialog-row { padding:0.2em; border-bottom: 1px solid #777; }'+
	'.rater-dialog-row:nth-child(even) { background-color:#e0e0e0; }'+
	'.rater-dialog-row > div:nth-child(2) { clear:left; }'+
	'.rater-dialog-row > div > span { padding-right:0.5em; white-space:nowrap; }'+
	'.rater-dialog-para-label { cursor:help; padding-right:0; }'+
	'.rater-dialog-para-code { font-family:monospace; font-size:123%; padding-right:0; }'+
	'.rater-dialog-para-code::before { content:"|"; }'+
	'.rater-dialog-para-code::after { content:"="; }'+
	'.rater-dialog-dropdown { width:5em; margin:0 0.2em; }'+
	'.rater-dialog-textInputContainer input { width:6em; margin:0 0.15em; }'+
	'.rater-dialog-autofill { border:1px dashed #cd20ff; padding:0.2em; margin-right:0.2em; }'+
	'.rater-dialog-autofill::after { content:"autofilled"; color:#cd20ff; font-weight:bold; font-size:96%; }'+
	/* ----- Diff styles ---------------------------------------------------------------------- */
	'table.diff, td.diff-otitle, td.diff-ntitle { background-color: white; }'+
	'td.diff-otitle, td.diff-ntitle { text-align: center; }'+
	'td.diff-marker { text-align: right; font-weight: bold; font-size: 1.25em; }'+
	'td.diff-lineno { font-weight: bold; }'+
	'td.diff-addedline, td.diff-deletedline, td.diff-context { font-size: 88%; vertical-align: top; white-space: -moz-pre-wrap; white-space: pre-wrap; }'+
	'td.diff-addedline, td.diff-deletedline { border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; }'+
	'td.diff-addedline { border-color: #a3d3ff; }'+
	'td.diff-deletedline { border-color: #ffe49c; }'+
	'td.diff-context { background: #f3f3f3; color: #333333; border-style: solid; border-width: 1px 1px 1px 4px; border-color: #e6e6e6; border-radius: 0.33em; }'+
	'.diffchange { font-weight: bold; text-decoration: none; }'+
	'table.diff { border: none; width: 98%; border-spacing: 4px;'+
		/* Ensure that colums are of equal width: */ 'table-layout: fixed; }'+
	'td.diff-addedline .diffchange, td.diff-deletedline .diffchange { border-radius: 0.33em; padding: 0.25em 0; }'+
	'td.diff-addedline .diffchange {	background: #d8ecff; }'+
	'td.diff-deletedline .diffchange { background: #feeec8; }'+
	'table.diff td {	padding: 0.33em 0.66em; }'+
	'table.diff col.diff-marker { width: 2%; }'+
	'table.diff col.diff-content { width: 48%; }'+
	'table.diff td div {'+
		/* Force-wrap very long lines such as URLs or page-widening char strings. */
		'word-wrap: break-word;'+
		/* As fallback (FF<3.5, Opera <10.5), scrollbars will be added for very wide cells
		   instead of text overflowing or widening */
		'overflow: auto;'+
	'}'
);
	
/* ========== API =============================================================================== */
var API = new mw.Api( {
    ajax: {
        headers: { 
			'Api-User-Agent': 'Rater/' + config.script.version + 
				' ( https://en.wikipedia.org/wiki/User:Evad37/Rater )'
		}
    }
} );
/* ---------- API for ORES ---------------------------------------------------------------------- */
API.getORES = function(revisionID) {
	return $.get('https://ores.wikimedia.org/v3/scores/enwiki?models=wp10&revids='+revisionID);
};
/* ---------- Raw wikitext ---------------------------------------------------------------------- */
API.getRaw = function(page) {
	var gotRaw = $.Deferred();
	
	var request = $.get('https:' + config.mw.wgServer + mw.util.getUrl(page, {action:'raw'}))
	.done(function(data) {
		if ( !data ) {
			gotRaw.reject('ok-but-empty');
			return;
		}
		gotRaw.resolve(data);
	})
	.fail(function(){
		status = request.getResponseHeader('status');
		gotRaw.reject('http', {textstatus: status || 'unknown'});
	});
	
	return gotRaw;
};

/* ========== Additional config & set up ======================================================== */
// Make OOjs UI window manager
config.windowManager = new OO.ui.WindowManager();
// - place above Morebits SimpleWindow, which has z-index of ~1000
config.windowManager.$element.css('z-index', '2000').appendTo('body');
// Get list of banners
config.gotListOfBanners = $.Deferred(); 
(function getListOfBanners() {
	var bannerNames = [];
	
	var processQuery = function(result) {
		if ( !result.query || !result.query.categorymembers ) {
			// No results
			
			// TODO: error or warning ********
			config.gotListOfBanners.reject();
			
			return;
		}
		
		// Gather titles into array - excluding "Template:" prefix
		var resultTitles = result.query.categorymembers.map(function(info) {
			return info.title.slice(9);
		});
		Array.prototype.push.apply(bannerNames, resultTitles);
		
		// Continue query if needed
		if ( result.continue ) {
			doApiQuery($.extend(query, result.continue));
			return;
		}
		
		config.banners = bannerNames;
		config.bannerOptions = config.banners.map(function(bannerName) {
			return {
				data:  bannerName,
				label: bannerName.replace('WikiProject ', '')
			};
		});
		config.gotListOfBanners.resolve();
		
	};

	var query = {
		action: 'query',
		format: 'json',
		list: 'categorymembers',
		cmtitle: 'Category:WikiProject banners with quality assessment',
		cmprop: 'title',
		cmnamespace: '10',
		cmlimit: '500'
	};
	
	var doApiQuery = function(q) {
		API.get( q )
		.done( processQuery )
		.fail( function(code, jqxhr) {
			console.warn('[Rater] ' + extraJs.makeErrorMsg(code, jqxhr, 'Could not retrieve pages from [Category:WikiProject banners with quality assessment]'));
			config.gotListOfBanners.reject();
		} );
	};
	
	doApiQuery(query);
})();

/* ========== Page class ======================================================================== */
// Extended version of mw.Title <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Title>
/**
 * @class Page
 * @constructor
 * @param {string} title
 *  Title of the page (can be URI encoded)
 * @throws {Error} When the title is invalid
 */
var Page = function(title) {
	try {
		mw.Title.call(this, decodeURIComponent(title));
	} catch(e) {
		throw new Error('Unable to parse title "'+title+'"'); 
	}
	this.talk = null;
	this.subject = null;
	this.banners = [];
};
/**
 * Page.newFromText
 * Constructor with a null return instead of an exception for invalid titles.
 *
 * @static
 * @param {string} t
 *  Title of the page
 * @return {Page|null} A valid Page object or null if the title is invalid
 */
Page.newFromText = function(t) {
	if ( mw.Title.newFromText(t) ) {
		return new Page(t);
	} else {
		return null;
	}
};
// ---------- Page prototype -------------------------------------------------------------------- */
// Inherit from mw.Title
Page.prototype = Object.create(mw.Title.prototype);
Page.prototype.constructor = Page;
// Additional functions
/**
 * getTalk
 *
 * Get the name of the page's talk page (for subject-space pages) or the page itself (for
 * talk-space pages)
 *
 * @return {string} Talk page name, includng namespace prefix
 */
Page.prototype.getTalk = function() {
	if ( this.talk === null ) {
		// talk page not yet set, so set it now
		if ( this.getNamespaceId()%2 === 1 ) {
			// Page is itself a talk page
			this.talk = this.getPrefixedText();
		} else {
			this.talk = mw.Title.newFromText(
				this.getMain(),
				this.getNamespaceId()+1
			).getPrefixedText();
		}
	}
	return this.talk;
};
/**
 * getSubject
 *
 * Get the name of the page's subject page (for talk-space pages) or the page itself (for
 * subject-space pages)
 *
 * @return {string} Subject page name, includng namespace prefix
 */
Page.prototype.getSubject = function() {
	if ( this.subject === null ) {
		// subject page not yet set, so set it now
		if ( this.getNamespaceId()%2 === 0 ) {
			// Page is itself a subject page
			this.subject = this.getPrefixedText();
		} else {
			this.subject = mw.Title.newFromText(
				this.getMain(),
				this.getNamespaceId()-1
			).getPrefixedText();
		}
	}
	return this.subject;
};
/**
 * getListasAutofill
 *
 * Get the autofill value for the "listas" parameter ('Last, First Middle+', no disambiguation) 
 *
 * @return {string} autofill value for "listas" parameter
 */
Page.prototype.getListasAutofill = function() {
	var name = this.getMainText().replace(/\s\(.*\)/, '');
	var lastSpaceIndex = name.lastIndexOf(' ');
	return name.slice(lastSpaceIndex+1) + ', ' + name.slice(0, lastSpaceIndex);
};
/**
 * getRedirectOrPrefixedText
 *
 * Get the page name of this page's redirect target (if applicable), or of this page itself
 *
 * @return {string} page name, with namespace prefix
 */	
Page.prototype.getRedirectOrPrefixedText = function() {
	return ( this.redirectsTo ) ? this.redirectsTo.getPrefixedText() : this.getPrefixedText();
};
/**
 * getRedirectOrMainText
 *
 * Get the page name, without the namespace prefix, of this page's redirect target (if applicable),
 * or of this page itself
 *
 * @return {string} page name, without namespace prefix
 */	
Page.prototype.getRedirectOrMainText = function() {
	return ( this.redirectsTo ) ? this.redirectsTo.getMainText() : this.getMainText();
};
/**
 * getBannerFromNameOrRedirect
 *
 * Get one of this page's banners (Template objects) from either its page name, or from the name of
 * the page it redirects to.
 *
 * @param {string} bannerNameOrRedirect
 * @return {Template|boolean} Template object if found, or `false` if not found
 */	
Page.prototype.getBannerFromNameOrRedirect = function(bannerNameOrRedirect) {
	if ( !this.banners ) {
		return false;
	}
	
	var toCheckPrefixedText = mw.Title.newFromText('Template:'+bannerNameOrRedirect).getPrefixedText();
	
	for ( var i=0; i<this.banners.length; i++ ) {
		if (
			this.banners[i].getPrefixedText() === toCheckPrefixedText ||
			this.banners[i].getRedirectOrPrefixedText() === toCheckPrefixedText
		) {
			return this.banners[i];
		}
	}
	return false;
};
/**
 * getTalkpageTopSection
 *
 * Retrieve the wikitext of the top section of the talk page, and store it as {this}.oldTopSection
 *
 * @return {jQuery.Deferred} Deferred object: resolved once oldTopSection is set, or rejected
 *  if the Api request fails
 */
Page.prototype.getTalkpageTopSection = function() {
	var self = this;
	var gotTalkpageTopSection = $.Deferred();
	
	var processTalk = function (result) {
		var id = result.query.pageids;		
		self.oldTopSection = ( id < 0 ) ? '' : result.query.pages[id].revisions[0]['*'];
		gotTalkpageTopSection.resolve();
	};
	
	API.get( {
		action: 'query',
		prop: 'revisions',
		rvprop: 'content',
		rvsection: '0',
		titles: self.getTalk(),
		indexpageids: 1
	} )
	.done( processTalk )
	.fail( gotTalkpageTopSection.reject );	
	
	return gotTalkpageTopSection;
};
/**
 * getLatestSubjectRevisionID
 *
 * Retrieve the revision ID of subject page's latest revision, and store it as
 * {this}.latestSubjectRevisionID
 *
 * @return {jQuery.Deferred} Deferred object: resolved once latestSubjectRevisionID is set, or rejected
 *  if the Api request fails
 */
Page.prototype.getLatestSubjectRevisionID = function() {
	
	var self = this;
	var gotRevisionID = $.Deferred();
	
	if  ( config.mw.wgNamespaceNumber === 0 && config.mw.wgRevisionId !== 0 ) {
		self.latestSubjectRevisionID = config.mw.wgRevisionId;
		return gotRevisionID.resolve();
	}
	
	var processRevision = function(result) {		
		var id = result.query.pageids;
		if ( id < 0 ) {
			gotRevisionID.reject();
			return;
		}
		self.latestSubjectRevisionID = result.query.pages[id].revisions[0].revid;
		gotRevisionID.resolve();
	};
	
	API.get( {
		action: 'query',
		format: 'json',
		prop: 'revisions',
		titles: self.getSubject(),
		rvprop: 'ids',
		indexpageids: 1
	} )
	.done( processRevision )
	.fail( gotRevisionID.reject );	
	
	return gotRevisionID;	
};
/**
 * getOresScore
 *
 * Retrieve the ORES score of subject page's latest revision, and store it as
 * {this}.oresScore
 *
 * @return {jQuery.Deferred} Deferred object: resolved once oresScore is set, or rejected
 *  if the Api request fails
 */
Page.prototype.getOresScore = function() {
	var self = this;
	var gotOresScore = $.Deferred();
	
	$.when( self.latestSubjectRevisionID || self.getLatestSubjectRevisionID() )
	.done( function() {
		API.getORES(self.latestSubjectRevisionID)
		.done(function(result) {
			var data = result.enwiki.scores[self.latestSubjectRevisionID].wp10;
			if ( data.error ) {
				gotOresScore.reject(data.error.type, data.error.message);
				return;
			}
			self.oresScore = data.score.prediction;
			gotOresScore.resolve();
		})
		.fail( gotOresScore.reject );
	})
	.fail( gotOresScore.reject );
	
	return gotOresScore;
};
/**
 * getSubjectRawWikitext
 *
 * Get the raw wikitext of subject page
 *
 * @return {jQuery.Deferred} Deferred object: resolves with the raw wikitext, or is rejected with
 *  http error details if the request fails 
 */
Page.prototype.getSubjectRawWikitext = function() {
	return API.getRaw(this.getSubject());
};
/**
 * makeEdit
 *
 * Makes the edit to the talk page
 *
 * @return {jQuery.Deferred} Deferred object: resolved if the edit is done, or is rejected with
 *  Api error details if the request fails
 */
Page.prototype.makeEdit = function() {
	var self = this;
	var editMade = $.Deferred();
	
	API.postWithToken( 'csrf', {
		action: 'edit',
		title: self.getTalk(),
		text: self.makeNewTopSection(),
		section: 0,
		summary: self.makeEditSummary() + config.script.advert
	} )
	.done( editMade.resolve )
	.fail( editMade.reject );
	return editMade;
};
/**
 * makePreview
 *
 * Make HTML of a preview of the edit
 *
 * @return {jQuery.Deferred} Deferred object: resolves with the preview HTML, or is rejected with
 *  Api error details if the request fails 
 */
Page.prototype.makePreview = function() {
	var self = this;
	var madePreview = $.Deferred();
	
	API.get({
		action: 'parse',
		contentmodel: 'wikitext',
		text: self.makeNewTopSection(),
		title: self.getTalk()
	})
	.done(function(result) {
		if ( !result || !result.parse || !result.parse.text || !result.parse.text['*'] ){
			madePreview.reject('Empty result');
		}
		madePreview.resolve(result.parse.text['*']);
	})
	.fail(madePreview.reject);
	
	return madePreview;
};
/**
 * makeDiff
 *
 * Make HTML of a diff to the current wikitext.
 *
 * @return {jQuery.Deferred} Deferred object: resolves with the diff HTML, or is rejected with
 *  Api error details if the request fails 
 */
Page.prototype.makeDiff = function() {
	var self = this;
	var madeDiff = $.Deferred();
	
	API.get({
		action: "compare",
		format: "json",
		fromtext: self.oldTopSection,
		fromcontentmodel: "wikitext",
		totext: self.makeNewTopSection(),
		tocontentmodel: "wikitext",
		prop: "diff"
	})
	.done(function(result) {
		if ( !result || !result.compare || !result.compare['*'] ){
			madeDiff.reject('Empty result');
			return;
		}
		var diffTable = $('<table>').append(
			$('<tr>').append(
				$('<th>').attr({'colspan':'2', 'scope':'col'}).text('Latest revision'),
				$('<th>').attr({'colspan':'2', 'scope':'col'}).text('New text')
			),
			result.compare['*']
		);
		madeDiff.resolve(diffTable);
	})
	.fail(madeDiff.reject);
	
	return madeDiff;
};
/**
 * makeNewTopSection
 *
 * Make wikitext for the edited top section.
 *
 * @return {string} wikitext
 */
Page.prototype.makeNewTopSection = function() {
	var self = this;
	
	var wikitext = {
		above: '',
		projects: '',
		below: ''
	};

	if ( self.oldTopSection && self.banners ) {
		var firstBannerIndex = self.banners.reduce(function(currentMinIndex, banner) {
			if ( banner.isNew() ) { 
				return currentMinIndex;
			}
			return Math.min(
				currentMinIndex,
				self.oldTopSection.indexOf(banner.rawWikitext)
			);
		}, Infinity);
		var afterLastBannerIndex = self.banners.reduce(function(currentMaxIndex, banner) {
			if ( banner.isNew() ) { 
				return currentMaxIndex;
			}
			return Math.max(
				currentMaxIndex,
				self.oldTopSection.indexOf(banner.rawWikitext) + banner.rawWikitext.length
			);
		}, -1);
		
		if ( firstBannerIndex >= afterLastBannerIndex ) {
			// TODO: ask for user confirmation
			// Place new banners at end of top section
			wikitext.projects = self.oldTopSection;
		} else {
			wikitext.above = self.oldTopSection.slice(0, firstBannerIndex).trim();
			wikitext.projects = self.oldTopSection.slice(firstBannerIndex, afterLastBannerIndex).trim();
			wikitext.below = self.oldTopSection.slice(afterLastBannerIndex).trim();
		}
		
	}
	
	wikitext.projects = self.banners.reduce(function(newWikitext, banner) {
		// Not touched (and not new, and redirect not bypassed)
		if ( $.isEmptyObject(banner.touched) && !banner.isNew() && !banner.bypassRedirect ) {
			return newWikitext;
		}
		// Marked for removal
		if ( banner.remove ) {
			if ( banner.isNew() ) { return newWikitext; }
			return newWikitext.replace(banner.rawWikitext, '');
		}
		// Existing banner that's been modified
		if ( !banner.isNew() ) {
			return newWikitext.replace(banner.rawWikitext.trim(), banner.buildWikitext());
		}
		// New banner
		return newWikitext += '\n' + banner.buildWikitext();
	}, wikitext.projects);
	
	return (wikitext.above + '\n' + wikitext.projects + '\n' + wikitext.below).trim();
};
/**
 * makeEditSummary
 *
 * Make the edit summary for the edit.
 *
 * @return {string} edit summary
 */
Page.prototype.makeEditSummary = function() {
	return this.banners.reduce(function(changes, banner) {
		// Not touched (and not new, and redirect not bypassed)
		if ( $.isEmptyObject(banner.touched) && !banner.isNew() && !banner.bypassRedirect ) {
			return changes;
		}
		// New and removed, no action needed
		if ( banner.remove && banner.isNew() ) {
			return changes;
		}
		
		// Symbol
		var symbol = '';
		if ( banner.remove ) {
			symbol = '−';
		} else if ( banner.isNew() ) {
			symbol = '+';
		}
		// Transclusion name, without WikiProject prefix
		var name = banner.getTransclusionName().replace('WikiProject ','');
		// Ratings, if touched
		var rating = '';
		if ( !banner.remove ) {
			var classRating = ( banner.touched.class ) ? banner.parameters.class.trim() : '';
			var impRating = ( banner.touched.importance ) ? banner.parameters.importance.trim() : '';
			if ( classRating && impRating ) {
				rating = classRating + '/' + impRating;
			} else {
				rating = classRating || impRating || '';
			}
			if ( rating ) {
				rating = ' (' + rating + ')';
			}
		}
		return changes += ' ' + symbol + name + rating + ';';
	}, 'Assessment:').slice(0,-1);
};
/**
 * setRedirectsTo
 *
 * If this Page is a redirect, set the redirect targart as {this}.redirectsTo
 *
 * @param {Page[]|null} Array of page objects to work on instead of this page
 * @return {jQuery.Deferred} Deferred object: resolved when all Pages have been processed, or is
 *  rejected with Api error details if the request fails 
 */
Page.prototype.setRedirectsTo = function(pageObjects) {
	var gotRedirects = $.Deferred();
	
	if ( pageObjects == null ) {
		pageObjects = this;
	}
	
	var pageObjectsToCheck = ( $.isArray(pageObjects) ) ? pageObjects : [pageObjects];

	var processRedirects = function(result) {
		if ( !result || !result.query ) {
			gotRedirects.reject();
			return;
		}
		if ( result.query.redirects ) {
			$.each(result.query.redirects, function(_index, redirect) {
				for ( var i=0; i<pageObjectsToCheck.length; i++ ) {
					if ( pageObjectsToCheck[i].getPrefixedText() === redirect.from ) {
						pageObjectsToCheck[i].redirectsTo = Page.newFromText(redirect.to);
						break;
					}
				}
			});
		}
		gotRedirects.resolve();
	};
	
	API.get({
		'action': 'query',
		'format': 'json',
		'titles': pageObjectsToCheck.map(function(pageObj) {
			return pageObj.getPrefixedText();
		}),
		'redirects': 1
	})
	.done( processRedirects )
	.fail( gotRedirects.reject );
	
	return gotRedirects;
};
/**
 * setTemplatesBanners
 *
 * Parses {this}.oldTopSection for banners, makes Template objects for each banner, and sets
 * {this}.banners to an Array of those Template objects. Will also #setRedirectsTo and
 * #setParamDataAndSuggestions for the Template objects.
 *
 * @return {jQuery.Deferred} Deferred object: resolved when all banners have been set processed, or
 *  is rejected if an Api request fails
 */
Page.prototype.setTemplatesBanners = function() {
	var self = this;
	var gotTemplatesBanners = $.Deferred();
	
	if ( self.oldTopSection === '' ) {
		return gotTemplatesBanners.resolve();
	}

	// Initially finds top-level templates only (not templates within template parameters)
	var makeTemplatesObjectsArray = function(wikitext) {
		// Reset lastindex of regex pattern so that test will work
		config.regex.template.lastIndex = 0;
		if ( !config.regex.template.test(wikitext) ) {
			return [];
		}
		return wikitext.match(config.regex.template).map(Template.newFromRawWikitext);
	};
	var talkpageTemplates = makeTemplatesObjectsArray(self.oldTopSection);
	if ( !talkpageTemplates.length ) {
		return gotTemplatesBanners.resolve();
	}
	
	// Find sub-templates within WikiProject banner shell	
	var isShellTemplate = function(templateObject) {
		return -1 !== $.inArray(templateObject.getMainText(), config.shellTemplates);
	};
	var shellTemplate = talkpageTemplates.filter(isShellTemplate)[0];
	if ( shellTemplate && shellTemplate.parameters[1] ) {
		var talkpageSubtemplates = makeTemplatesObjectsArray(shellTemplate.parameters[1]);
		// Merge subtemplates into main array
		$.merge(talkpageTemplates, talkpageSubtemplates);
	}

	// Check for redirects, then filter out non-banners
	$.when(self.setRedirectsTo(talkpageTemplates), config.gotListOfBanners)
	.done(function(){
		var talkpageBanners = talkpageTemplates.filter(function(templateObject) {
			return -1 !== $.inArray(templateObject.getRedirectOrMainText(), config.banners);
		});
		
		self.banners = talkpageBanners;
		
		// Retrieve TemplateData
		var retrieveTemplateDatas = self.banners.map(function(templateObject) {
			return templateObject.setParamDataAndSuggestions()
			.fail(function(code, jqxhr){
				console.log('[Rater] Failed to retrieve TemplateData for ' + templateObject.getPrefixedText() +
				( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr));
			});
		});
		// Always resolve, because we can still work without the TemplateData
		$.when.apply(null, retrieveTemplateDatas).always(function() {
			gotTemplatesBanners.resolve();
		});
		
	})
	.fail(function() { gotTemplatesBanners.reject(); });
	return gotTemplatesBanners;
};

/* ========== Template class ==================================================================== */
// Extended version of Page class for banner templates
/**
 * @class Template
 * @constructor
 * @param {string} title
 *  Title of the template, including namespace prefix
 * @param {object|null} parameters
 *  Object of key:val pairs for parameter names (keys) and their values (vals)
 *  Title of the page (can be URI encoded)
 * @param {string|null} rawWikitext
 *  Wikitext the object was derived from, e.g. "{{Templatename|para1=val1|para2=val2}}"
 * @throws {Error} When the title is invalid
 */
var Template = function(title, parameters, rawWikitext) {
	try {
		Page.call(this, decodeURIComponent(title));
	} catch(e) {
		throw new Error('Unable to parse template title "'+title+'"'); 
	}
	this.parameters = parameters || {};
	this.rawWikitext = rawWikitext || null;
	this.isProjectBanner = null;
	this.touched = {};
};
/**
 * Template.makeParamsObject
 *
 * Converts a string containg template parameters into an object of parameter names (or positons)
 * and their values.
 *
 * @static
 * @param {string} wikitext
 *  Wikitext containg parameters
 * @return {object} Object with key:value pairs correspoinding to |parameter=value pairs.
 */
Template.makeParamsObject = function(wikitext) {
	var params = {};
	var unnamedParamCount = 0;
	var parts = wikitext.match(config.regex.templateParams);
	for ( var i=0; i<parts.length; i++ ) {
		if ( parts[i].trim() === '|' ) {
			//Empty unnamed parameter, i.e. {{foo||bar}}
			unnamedParamCount++;
			continue;
		}
		var equalsIndex = parts[i].indexOf('=');
		if ( equalsIndex === -1 ) {
			//unnamed parameter
			unnamedParamCount++;
			params[unnamedParamCount.toString()] = parts[i].slice(1).trim();
		} else {
			params[parts[i].slice(1, equalsIndex).trim()] = parts[i]
				.slice(equalsIndex+1).trim();
		}
	}
	return params;
};
/**
 * Page.newFromRawWikitext
 *
 * Constructor from raw wikitext as used on a wiki page.
 *
 * @static
 * @param {string} rawWikitext
 *  Raw wikitext of the template, e.g. "{{TemplateName|value1|para2=value2|para3=value3}}"
 * @return {Template|null} A valid Template object or null if the title is invalid
 * @throws {Error} If unable to parse the Raw wikitext
 */
// Constructor from raw wikitext, i.e. 
Template.newFromRawWikitext = function(rawWikitext) {
	// Reset lastindex of regex pattern so that exec will work
	config.regex.template.lastIndex = 0;
	var parts = config.regex.template.exec(rawWikitext);
	if ( !parts || !parts[0] || !parts[1] ) {
		throw new Error('Unable to parse template from wikitext: ' + rawWikitext);
	}
	var params = ( parts[2] ) ? Template.makeParamsObject(parts[2]) : null;
	return new Template('Template:'+parts[1], params, parts[0]);
};
// ---------- Template prototype ---------------------------------------------------------------- */
// Inherited from Page
Template.prototype = Object.create(Page.prototype);
Template.prototype.constructor = Template;
// Additional functions
/**
 * isNew
 *
 * Check if template is new, i.e. wasn't added from raw wikitext
 *
 * @return {boolean}
 */
Template.prototype.isNew = function() {
	return this.rawWikitext === null;
};
/**
 * getParamValue
 *
 * Get the currently stored value of a parameter
 *
 * @param {string} param
 *  Name of parameter
 * @return {string|null} Value of parameter, or null if not found
 */
Template.prototype.getParamValue = function(param) {
	return this.parameters[param] || null;
};
/**
 * setParamValue
 *
 * Set the currently stored value of a parameter
 *
 * @param {string} param
 *  Name of parameter
 * @param {string} val
 *  Value to set
 */
Template.prototype.setParamValue = function(param, val) {
	this.parameters[param] = val;
	this.touched[param] = true;
};
/**
 * deleteParam
 *
 * Delete a parameter
 *
 * @param {string} param
 *  Name of parameter
 */
Template.prototype.deleteParam = function(param) {
	delete this.parameters[param];
	this.touched[param] = true;
};
/**
 * getTransclusionName
 *
 * Get the name of the template, without namespace prefix; or if {this}.bypassRedirect is true, get
 * the name of this template's redirect target, without namespace prefix.
 *
 * @return {string} Transclusion name
 */
Template.prototype.getTransclusionName = function() {
	if ( this.bypassRedirect ) {
		return this.redirectsTo.getMainText();
	}
	return this.getMainText();
};
/**
 * getLinkedName
 *
 * Get a link to the #getTransclusionName
 *
 * @return {jQuery} <a> element
 */
Template.prototype.getLinkedName = function() {
	if ( this.bypassRedirect ) {
		return extraJs.makeLink(this.redirectsTo.getPrefixedText(), this.redirectsTo.getMainText());
	}
	return extraJs.makeLink(this.getPrefixedText(), this.getMainText());
};

// Banner-specific functions
Template.prototype.parseClassesAndImportances = function() {
	var self = this;
	var parsed = $.Deferred();
	
	if ( self.classes && self.importances ) {
		return parsed.resolve();
	}
	
	var testWikitext = config.bannerDefaults.extendedClasses.map(function(c) {
		return '{{' + self.getMainText() + '|class=' + c + '}}';
	}).join('/n') +
	config.bannerDefaults.extendedImportances.map(function(i) {
		return '{{' + self.getMainText() + '|importane=' + i + '}}';
	}).join('/n');
	
	var processCategories = function(result) {
		var catsHtml = result.parse.categorieshtml['*'];
		self.classes = $.merge(
			$.merge([],	config.bannerDefaults.classes),
			config.bannerDefaults.extendedClasses.filter(function(c) {
				return catsHtml.indexOf(c+'-Class') !== -1;
			})
		);
		self.importances = $.merge(
			$.merge([],	config.bannerDefaults.importances),
			config.bannerDefaults.extendedImportances.filter(function(i) {
				return catsHtml.indexOf(i+'-importance') !== -1;
			})
		);
		parsed.resolve();
	};
		
	
	API.get({
		action: 'parse',
		title: 'Talk:Sandbox',
		text: testWikitext,
		prop: 'categorieshtml'
	}).done(processCategories)
	.fail(parsed.reject);
	
	return parsed;
};
Template.prototype.getDataForParam = function(key, paraName) {
	if ( !this.paramData ) {
		return null;
	}
	// If alias, switch from alias to preferred parameter name
	para = this.paramAliases[paraName] || paraName;	
	if ( !this.paramData[para] ) {
		return;
	}
	
	var data = this.paramData[para][key];
	// Data might actually be an object with key "en"
	if ( data && data.en && !$.isArray(data) ) {
		return data.en;
	}
	return data;
};

Template.prototype.setParamDataAndSuggestions = function() {
	var self = this;
	var paramDataSet = $.Deferred();
	
	if ( self.paramData ) { return paramDataSet.resolve(); }
	
	var processTemplatedata = function(result) {
		// Figure out page id (beacuse action=templatedata doesn't have an indexpageids option)
		var id = $.map(result.pages, function( _value, key ) { return key; });
		
		if ( result.pages[id].notemplatedata ) {
			// No TemplateData, so nothing more to do
			paramDataSet.resolve();
			return;
		}
		// ParamData
		self.paramData = result.pages[id].params;
		self.paramAliases = {};
		
		var extractExtraParamData = function(paraName, paraData) {
			// Extract aliases for easier reference later on
			if ( paraData.aliases.length ) {
				paraData.aliases.forEach(function(alias){
					self.paramAliases[alias] = paraName;
				});
			}
			// Extract allowed values array from description
			if ( paraData.description && /\[.*'.+?'.*?\]/.test(paraData.description.en) ) {
				try {
					var allowedVals = JSON.parse(
						paraData.description.en
						.replace(/^.*\[/,'[')
						.replace(/"/g, '\\"')
						.replace(/'/g, '"')
						.replace(/,\s*]/, ']')
						.replace(/].*$/, ']')
					);
					self.paramData[paraName].allowedValues = allowedVals;
				} catch(e) {
					console.warn('[Rater] Could not parse allowed values in description:\n  '+
					paraData.description.en + '\n Check TemplateData for parameter |' + paraName +
					'= in ' + self.getPrefixedText());
				}
			}
			// Make sure required/suggested parameters are present
			if ( (paraData.required || paraData.suggested) && !self.parameters[paraName] ) {
				// Check aliases, if any
				if ( paraData.aliases.length ) {
					var makeParaObject = function(val, name) {
						return {'name': name, 'value':val};
					};
					var isNonEmptyAlias = function(paraObj) {
						var isAlias = (-1 !== $.inArray(paraObj.name, paraData.aliases));
						if ( !isAlias ) { return false; }
						var isEmpty = !self.parameters[paraObj.name];
						if ( isEmpty ) {
							self.deleteParam(paraObj.name);
							return false;
						}
						return true;
					};
					aliasesPresent = $.map(self.parameters, makeParaObject)
					.filter(isNonEmptyAlias);
					if ( aliasesPresent.length ) {
						// At least one non-empty alias, so do nothing
						return;
					}
				}
				// No non-empty aliases, so set parameter to an empty string
				self.setParamValue(paraName, '');
			}
		};
		$.each(self.paramData, extractExtraParamData);
		
		//Suggestions
		self.parameterSuggestions = result.pages[id].paramOrder && result.pages[id].paramOrder
		.filter(function(paramName) {
			return ( paramName !== 'class' && paramName !== 'importance' );
		})
		.map(function(paramName) {
			var optionObject = {data: paramName};
			var label = self.getDataForParam(label, paramName);
			if ( label ) {
				optionObject.label = label + ' (|' + paramName + '=)';
			}
			return optionObject;
		});
		
		paramDataSet.resolve();
	};
			
	API.get({
		action: 'templatedata',
		titles: self.getRedirectOrPrefixedText(),
		redirects: 1,
		doNotIgnoreMissingTitles: 1,
	})
	.done( processTemplatedata )
	.fail( paramDataSet.reject );
	
	return paramDataSet;	
};

Template.prototype.makeParameterSuggestions = function() {
	var self = this;
	var gotParams = $.Deferred();
	
	if ( self.parameterSuggestions ) { return gotParams.resolve(); }
	
	var processTemplatedata = function(result) {
		// Figure out page id (beacuse action=templatedata doesn't have an indexpageids option)
		var id = $.map(result.pages, function( _value, key ) { return key; });
		
		if ( result.pages[id].notemplatedata || !result.pages[id].paramOrder ) {
			gotParams.reject();
			return;
		}
			
		self.parameterSuggestions = result.pages[id].paramOrder.filter(function(paramName) {
			return ( paramName !== 'class' && paramName !== 'importance' );
		})
		.map(function(paramName) {
			return {data: paramName};
		});
		
		gotParams.resolve();
	};
			
	API.get({
		action: 'templatedata',
		titles: self.getRedirectOrPrefixedText(),
		doNotIgnoreMissingTitles: 1
	})
	.done( processTemplatedata )
	.fail( gotParams.reject );
	return gotParams;
};

Template.prototype.getUnusedParamterSuggestions = function() {
	return this.parameterSuggestions || [];
	/*
	if ( !this.parameterSuggestions ) {
		return [];
	}
	
	return this.parameterSuggestions.filter(function(param) {
		return self.templateObject.parameters[param.data] == null;
	});
	*/
};

Template.prototype.buildWikitext = function() {
	var paras = '';
	if ( !$.isEmptyObject(this.parameters) ) {
		paras = $.map(this.parameters, function(val, name) {
			if ( val === null || val === '' ) { return '';}
			return ' |'+name+'='+val;
		}).join('');
	}		
	return '{{' + this.getTransclusionName() + paras + '}}';
};
	
/* ========== ComboBoxInputPrompt class ========================================================== */
// Subclass of OOjs UI ProcessDialog 
var ComboBoxInputPrompt = function( config ) {
  ComboBoxInputPrompt.super.call( this, config );
};
OO.inheritClass( ComboBoxInputPrompt, OO.ui.ProcessDialog );

// Specify a name for .addWindows()
ComboBoxInputPrompt.static.name = 'comboBoxInput';
// Specify the static configurations: title and action set
ComboBoxInputPrompt.static.actions = [
  { flags: 'primary', label: 'Add', action: 'add' },
  { flags: 'safe', label: 'Cancel' }
];

// Customize the initialize() function to add content and layouts: 
ComboBoxInputPrompt.prototype.initialize = function () {
	ComboBoxInputPrompt.super.prototype.initialize.call( this );
	this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	this.content = new OO.ui.FieldsetLayout();

	this.input = new OO.ui.ComboBoxInputWidget( {
		$overlay: this.$overlay,
		//validate: 'non-empty',
		//autofocus: true,
		menu: {
			filterFromInput: true
		}
	});

	this.input.$pending.addClass( 'oo-ui-pendingElement-pending' );

	this.field = new OO.ui.FieldLayout( this.input, {
		label: ' ',//placeholder
		align: 'top'
	} );

	this.content.addItems([ this.field ]);
	this.panel.$element.append( this.content.$element );

	this.$body.append( this.panel.$element );

	this.input.connect( this, { 'change': 'onInputChange' } );
	this.input.connect( this, { 'enter': 'onEnterPress' } );
};

// Specify any additional functionality required by the window (disable opening an empty URL, in this case)
ComboBoxInputPrompt.prototype.onInputChange = function ( value ) {
  this.actions.setAbilities( {
    add: !!value.length 
  } );
};
ComboBoxInputPrompt.prototype.onEnterPress = function() {
	this.executeAction('add');
};


// Specify the dialog height (or don't to use the automatically generated height).

ComboBoxInputPrompt.prototype.getBodyHeight = function () {
  return this.panel.$element.outerHeight( true )*1.1;
};


// Use getSetupProcess() to set up the window with data passed to it at the time of opening.
ComboBoxInputPrompt.prototype.getSetupProcess = function ( data ) {
	var self = this;
		
	data = data || {};
	return ComboBoxInputPrompt.super.prototype.getSetupProcess.call( this, data )
	.next( function () {
		if ( data.label ) {
			self.field.setLabel(data.label);
		}
		if ( data.help ) {
			self.field.setNotices([data.help]);
		}

		// Set up contents based on data
		$.when( data.optionsReady )
		.done(function() {
			self.input.setOptions(
				data.makeOptions()
			);
		})
		.fail(function() {
			if ( data.failnotice ) {
				self.field.setNotices(
					( data.help ) ? [data.help, data.failnotice] : [data.failnotice]
				);
				self.updateSize();
			}
		})
		.always(function() {
			self.input.$pending.removeClass( 'oo-ui-pendingElement-pending' );
		});
  }, this );
};

// Specify processes to handle the actions.
ComboBoxInputPrompt.prototype.getActionProcess = function ( action ) {
	var self = this;
	if ( action === 'add' ) {
		// Close dialog, passing through the input data
		return new OO.ui.Process( function () {
			self.close({input: self.input.getValue()});
		});
	}
	// Fallback to parent handler
	return ComboBoxInputPrompt.super.prototype.getActionProcess.call( this, action );
};

// Use the getTeardownProcess() method to perform actions whenever the dialog is closed. 
// This method provides access to data passed into the window's close() method 
// or the window manager's closeWindow() method.
ComboBoxInputPrompt.prototype.getTeardownProcess = function ( data ) {
  return ComboBoxInputPrompt.super.prototype.getTeardownProcess.call( this, data );
//  .first( function () {
//  // Perform any cleanup as needed
//  }, this );
};
	

/* ========== Dialog class ====================================================================== */
// Constructor
var Dialog = function(currentPage) {
	this.page = currentPage;
	
	// Make an new dialog/interface window	
	this.interfaceWindow = new Morebits.simpleWindow(
		Math.min(900, Math.floor(window.innerWidth*0.8)),
		Math.floor(window.innerHeight*0.9)
	);
	this.interfaceWindow.setTitle('Rater [v.'+config.script.version+']');
	this.interfaceWindow.addFooterLink('script documentation', 'WP:RATER');
	this.interfaceWindow.addFooterLink('feedback', 'WT:RATER');
	this.interfaceWindow.setContent(
		$('<div>')
		.attr('id', 'rater-dialog')
		.append(
			//$('<div>').attr('id', 'rater-dialog-header'),
			$('<div>').attr('id', 'rater-dialog-body')
		)
		.get(0)
	);
	$('a.ui-dialog-titlebar-close.ui-corner-all').remove();
	$('#rater-dialog').parent().css('background-color', '#f0f0f0');
	this.$footerButtons = $('#rater-dialog').parent().nextAll('.ui-dialog-buttonpane')
		.find('span.morebits-dialog-buttons');
	this.interfaceWindow.display();
};
// Overlay dialog (for previews, diffs, etc)
Dialog.showOverlayDialog = function(contentDeferred, heading) {
	var overlayDialog = new OO.ui.MessageDialog();
	config.windowManager.addWindows( [ overlayDialog ] );
	var instance = config.windowManager.openWindow( overlayDialog, {
		title: heading,
		message: 'Loading...',
		size: 'larger',
		actions: [ {
			action: 'accept',
			label: 'Close',
			flags: 'primary'
		} ]
	} );
	instance.opened.then( function() {
		contentDeferred.done(function(contentHtml){
			overlayDialog.$element.find('label.oo-ui-messageDialog-message').empty().after(contentHtml);
			overlayDialog.updateSize();
		})
		.fail(function(code, jqxhr) {
			overlayDialog.$element.find('label.oo-ui-messageDialog-message').empty().append(
				heading + ' failed.',
				( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr)
			);
		});
	});
	instance.closed.then(function(){ config.windowManager.clearWindows(); });
};

// ---------- Dialog prototype ------------------------------------------------------------------ */

// --- Basic manipulation: ---
// Append content to header
//Dialog.prototype.addToHeader = function($content) {
//	$('#rater-dialog-header').append($content);
//};
// Append content to body
Dialog.prototype.addToBody = function($content) {
	$('#rater-dialog-body').append($content);
};
// Add buttons to footer
Dialog.prototype.setFooterButtons = function($buttons, mode) {
	if ( mode === 'prepend' ) {
		this.$footerButtons.prepend($buttons);
	} else if ( mode === 'append' ) {
		this.$footerButtons.append($buttons);
	} else {
		this.$footerButtons.empty().append($buttons);
	}
};
// Clear dialog
Dialog.prototype.emptyContent = function() {
	$('#rater-dialog-body').empty();
};
// Display dialog
Dialog.prototype.display = function() {
	this.interfaceWindow.display();
};
// Reset height
Dialog.prototype.resetHeight = function() {
	this.interfaceWindow.setHeight(Math.floor(window.innerHeight*0.9));
};
// Close dialog
Dialog.prototype.close = function() {
	this.interfaceWindow.close();
};

// --- Make interface elements: ---
Dialog.icons = {
	'delete': 	{
		'source':	'/media/wikipedia/commons/thumb/1/18/OOjs_UI_icon_close-ltr.svg/40px-OOjs_UI_icon_close-ltr.svg.png',
		'tooltip':	'Remove template'
	},
	'clear':	{
		'source':	'/media/wikipedia/commons/thumb/5/54/OOjs_UI_icon_noWikiText-ltr.svg/40px-OOjs_UI_icon_noWikiText-ltr.svg.png',
		'tooltip':	'Clear parameters'
	},
	'bypass':	{
		'source':	'/media/wikipedia/commons/thumb/5/5d/OOjs_UI_icon_newline-rtl.svg/40px-OOjs_UI_icon_newline-rtl.svg.png',
		'tooltip':	'Bypass redirect'
	},
	'ores':		{
		'source':	'/media/wikipedia/commons/thumb/5/51/Objective_Revision_Evaluation_Service_logo.svg/40px-Objective_Revision_Evaluation_Service_logo.svg.png',
		'tooltip':	'Machine-predicted quality from ORES'
	},
	'redirect':	{
		'source':	'/media/wikipedia/en/thumb/8/89/Symbol_redirect_vote2.svg/40px-Symbol_redirect_vote2.svg.png',
		'tooltip':	'Page is a redirect'
	},
};

Dialog.makeIcon = function(iconName, clickable) {
	return $('<img>').attr({
		'src':		Dialog.icons[iconName].source,
		'title':	Dialog.icons[iconName].tooltip,
		'alt':		iconName,
		'width':	'20px',
		'height':	'20px'
	}).addClass('rater-dialog-'+iconName)
	.css( (clickable===false) ? {} : {'float':'left', 'cursor':'pointer', 'margin-right':'0.2em'});
};

Dialog.makeDropdown = function(values, selectedValue, param, data) {
	var $nullOption = $('<option>').attr('value', ' ').text(' ');
	
	var $dropdown = $('<select>')
	.addClass('rater-dialog-dropdown')
	.append( $nullOption )
	.append(
		values.map(function(val) {
			return $('<option>').attr('value', val).text(val);
		})
	);
	
	var $selected = ( selectedValue == null ) ? [] : $dropdown.children().filter(function(){
		return $(this).attr('value').toLowerCase() === selectedValue.toLowerCase();
	}).first();
	if ( $selected.length ) {
		$selected.attr('selected', 'selected');
	} else {
		$nullOption.attr('selected', 'selected');
	}
	
	if ( param == null ) {
		return $dropdown;
	}
	
	return $('<span>').addClass('rater-dialog-paraInput rater-dialog-dropdownContainer').append(
		( param ) ? Dialog.makeParamLabel(param, data && data.label, data && data.description) : '',
		$dropdown,
		( data && data.required) ? '' : $('<a>').attr('title', 'remove').text('x'),
		$('<wbr>')
	);

};

Dialog.makeParamCheckbox = function(currentVal, param, data) {
	currentVal = currentVal || '';
	
	var valueIndex = $.inArray(currentVal.toLowerCase(), data.allowedValues );	
	// If existing value isn't one of the allowed values (or no value), switch to makeParamTextInput
	if ( valueIndex === -1 && currentVal !== '') {
		return Dialog.makeParamTextInput(currentVal, param, data);		
	}
	// valueIndex will now be 0 ('checked' value) or 1 ('not checked' value) or -1 (no value)
	
	return $('<span>').addClass('rater-dialog-paraInput rater-dialog-checkboxContainer').append(
		Dialog.makeParamLabel(param, data && data.label, data && data.description),
		$('<input>')
			.attr('type', 'checkbox')
			.prop('checked', !valueIndex)
			.data('values', {
				'true': data.allowedValues[0] || '',
				'false': data.allowedValues[1] || ''
			}),
		( data && data.required) ? '' : $('<a>').attr('title', 'remove').text('x'),
		$('<wbr>')
	);
};

Dialog.makeParamTextInput = function(currentVal, param, data) {
	return $('<span>').addClass('rater-dialog-paraInput rater-dialog-textInputContainer').append(
		Dialog.makeParamLabel(param, data && data.label, data && data.description),
		$('<input>').attr('type', 'text').val(currentVal),
		( data && data.required) ? '' : $('<a>').attr('title', 'remove').text('x'),
		$('<wbr>')
	);
};

Dialog.makeParamLabel = function(param, labeltext, description) {
	// Make label
	var $label = $('<label>');
	if ( labeltext ) {
		$label.append(
			$('<span>').addClass('rater-dialog-para-label')
				.text(labeltext)
				.attr('title', '|'+param+'= ' + (description || '')),
			$('<span>').text(param).hide()
		);
	} else {
		$label.append(
			$('<span>').addClass('rater-dialog-para-code').text(param)
		);
	}
	return $label;
};
Dialog.makeParamInput = function(currentVal, param, data) {
	if ( !data || !data.allowedValues ) {
		return Dialog.makeParamTextInput(currentVal, param, data);
	}
	if ( data.allowedValues.length <=2 ) {
		return Dialog.makeParamCheckbox(currentVal, param, data);
	}
	return Dialog.makeDropdown(data.allowedValues, currentVal, param, data);
};

Dialog.makeButton = function(options) {
	var $button = new OO.ui.ButtonWidget(options).$element;
	$button.keypress(function(e) {
		if ( e.which == 13 ) { $(this).click(); }
	})
	.children().css({'padding-top':'0.5em','padding-bottom':'0.4em'});
	return $button;
};
Dialog.makeFramelessButton = function(options) {
	options.framed = false;
	var $button = new OO.ui.ButtonWidget(options).$element;
	$button.css('padding','0').keypress(function(e) {
		if ( e.which == 13 ) { $(this).click(); }
	}).children().css({'padding-top':'0.3em','padding-bottom':'0.3em'})
		.children().css('font-weight','normal');
	return $button;
};

Dialog.prototype.makeRow = function(templateObject) {
	var self = this;
	var $row = $('<div>');

	var setParamHandlers = function() {
		var $span = $(this);
		var param = $span.children('label').children().last().text();
		var $input = $span.children('input');
		var $a = $span.children('a');
		
		if ( $input.attr('type') === 'text' ) {
			$input.blur(function() {
				if ( templateObject.parameters[param] !== $input.val().trim() ) {
					templateObject.setParamValue(param, $input.val().trim());
				}
			});
			$a.click(function() {
				templateObject.deleteParam(param);
				self.rebuildRow($row, templateObject);
			});
			
		} else if ( $input.attr('type') === 'checkbox' ) {
			$input.change(function() {
				templateObject.setParamValue(param, $input.data('values')[$input.prop('checked')]);
			});
			$a.click(function() {
				templateObject.deleteParam(param);
				self.rebuildRow($row, templateObject);
			});
			
		} else {
			var $dropdown = $span.children('select');
			$dropdown.change(function() {
				var $thisVal = $(this).val() || '';
				if (
					templateObject.parameters[param] &&
					templateObject.parameters[param].toLowerCase() === $thisVal.toLowerCase()
				) {
					return;
				}
				templateObject.setParamValue(param, $thisVal);
			});			
		}
	};
	

	var removeTemplate = Dialog.makeIcon('delete').click(function() {
		templateObject.remove = true;
		templateObject.isProjectBanner = false;
		$row.remove();
	});
	
	var clearTemplate = Dialog.makeIcon('clear').click(function() {
		$.each(templateObject.parameters, function(paraName) {
			templateObject.setParamValue(paraName, null);
		});
		self.rebuildRow($row, templateObject);
	});
	
	var templateName = $('<span>').addClass('rater-dialog-templateName').append(
		'{{',
		templateObject.getLinkedName(),
		'}}'
	);

	var bypassRedirect = '';
	if ( templateObject.redirectsTo && !templateObject.bypassRedirect ) {
		bypassRedirect = Dialog.makeIcon('bypass').click(function() {
			templateObject.bypassRedirect = true;
			templateName.empty().append(
				'{{',
				templateObject.getLinkedName(),
				'}}'
			);
			$(this).remove();
		});
	}
	
	var classParam = Dialog.makeDropdown(
		templateObject.classes,
		templateObject.parameters.class,
		'class',
		{label:'Class', required:true}
	);
	classParam.addClass('rater-dialog-row-class');
	
	var importanceParam = Dialog.makeDropdown(
		templateObject.importances,
		templateObject.parameters.importance,
		'importance',
		{label:'Importance', required:true}
	);
	importanceParam.addClass('rater-dialog-row-importance');

	var addParam = $('<span>').append(
		Dialog.makeFramelessButton({
			label:	'[add parameter]',
			icon:	'tableAddColumnBefore'
		})
	);
	addParam.click(function() {		
		var sugesstionsReady = templateObject.makeParameterSuggestions();
		var prompt = new ComboBoxInputPrompt();
		config.windowManager.addWindows( [ prompt ] );
		
		instance = config.windowManager.openWindow( prompt, {
			label: 'Add parameter',
			optionsReady: sugesstionsReady,
			makeOptions: function() {
				return templateObject.getUnusedParamterSuggestions();
			},
			failnotice: new OO.ui.HtmlSnippet(
				$('<span>').css({'color':'#555', 'font-size':'92%'}).append(
					'Parameter suggestions not available.',
					$('<p>').append(
						'This WikiProject banner has not been configured for use with this tool. See the ',
						extraJs.makeLink('User:Evad37/rater#TemplateData_quick_tutorial', 'TemplateData quick tutorial'),
						' or ask for help on ',
						extraJs.makeLink('User talk:Evad37/rater.js', ' the script\'s talk page'),
						'.'
					)
				)
			)
		} );
		instance.opened.then( function() {
			prompt.input.focus();
		});
		instance.closed.then( function ( data ) {
			config.windowManager.clearWindows();
			if ( !data || !data.input ) {
				// No input data - ie cancelled
				return;
			}
			if ( templateObject.parameters[data.input] != null ) {
				alert('There is already a ' + data.input + ' parameter!');
				return;
			}
			templateObject.parameters[data.input] = templateObject.getDataForParam('autovalue', data.input) || '';
			self.rebuildRow($row, templateObject);
		} );
	});
	
	var otherParams = $('<div>').append(
		$.map(templateObject.parameters, function(val, param) {
			if (
				val === null ||
				param === 'class' ||
				param === 'importance' //||
				//( param === 'listas' &&
				//  templateObject.getRedirectOrMainText() === 'WikiProject Biography' )
			) {
				return '';
			}
			var data = ( !templateObject.paramData ) ? null : {
				label: templateObject.getDataForParam('label', param),
				description: templateObject.getDataForParam('description', param),
				allowedValues: templateObject.getDataForParam('allowedValues', param),
				suggested: templateObject.getDataForParam('suggested', param),
				required: templateObject.getDataForParam('required', param),
				autovalue: templateObject.getDataForParam('autovalue', param)
			};
			return Dialog.makeParamInput(val, param, data);
		}),
		addParam
	);
	
	$().add(classParam)
	.add(importanceParam)
	.add(otherParams.children('span.rater-dialog-paraInput'))
	.each(setParamHandlers);
	
	return $row.addClass('rater-dialog-row').append(
		$('<div>').append(
			removeTemplate,
			clearTemplate,
			templateName,
			bypassRedirect,
			classParam,
			importanceParam
			//listasParam
		),
		otherParams
	);
};

Dialog.prototype.makeActionsRow = function() {
	var self = this;
	
	var addProject = Dialog.makeButton({
		label:	'Add WikiProject',
		icon:	'add',
		flags:	['progressive']
	})
	.click(function() {
		var prompt = new ComboBoxInputPrompt();
		config.windowManager.addWindows( [ prompt ] );
		instance = config.windowManager.openWindow( prompt, {
			label: 'Add Template:',
			optionsReady: config.gotListOfBanners,
			makeOptions: function() { return config.bannerOptions; }
		} );
		instance.opened.then( function() {
			prompt.input.focus();
		});
		instance.closed.then( function ( data ) {
			config.windowManager.clearWindows();
			
			if ( !data || !data.input ) { return; }
			
			var existingBanner = self.page.getBannerFromNameOrRedirect(data.input);
			
			if ( existingBanner && !existingBanner.remove ) {
				alert('There is already a {{' + existingBanner.getTransclusionName() + '}} banner!');
				return;
			}
			
			var templateObject;
			
			if ( existingBanner ) {
				existingBanner.remove = false;
				existingBanner.parameters = {};
				templateObject = existingBanner;
			} else {
				templateObject = new Template('Template:'+data.input);
				templateObject.isProjectBanner = true;
				self.page.banners.push(templateObject);
			}
			
			// Retrieve extra data -- but always resolve because we can still work without it
			var redirectsToDeferred =  $.Deferred();			
			$.when(!!existingBanner || templateObject.setRedirectsTo())
			.always(function() { redirectsToDeferred.resolve(); });

			var classesAndImportancesDeferred = $.Deferred();
			$.when(!!existingBanner || templateObject.parseClassesAndImportances())
			.always(function() { classesAndImportancesDeferred.resolve(); });
			
			var templateDataDeferred = $.Deferred();
			$.when(!!existingBanner || templateObject.setParamDataAndSuggestions())
			.always(function() { templateDataDeferred.resolve(); });

			$.when(redirectsToDeferred, classesAndImportancesDeferred, templateDataDeferred)		
			.then( function() {
				var newRow = self.makeRow(templateObject).insertBefore('#rater-dialog-actions');
				self.autofillParams(newRow);
			} );
		} );
	});
	
	var removeAll = Dialog.makeButton({
		label:	'Remove all',
		icon:	'close',
		flags:	['destructive']
	}).click(function() {
		$.each(self.page.banners, function(_index, templateObject) {
			templateObject.remove = true;
		});
		$('div.rater-dialog-row').not('#rater-dialog-actions').remove();
	});
	
	var clearAll = Dialog.makeButton({
		label:	'Clear all',
		icon:	'noWikiText'
	}).click(function() {
		$.each(self.page.banners, function(_index, templateObject) {
			$.each(templateObject.parameters, function(paraName) {
				templateObject.setParamValue(paraName, null);
			});
		});
		self.refresh();
	});

	var bypassAllRedirects = '';
	if ( $('img.rater-dialog-bypass').length ) {
		bypassAllRedirects = Dialog.makeButton({
			label:	'Bypass redirects',
			icon:	'arrowNext'
		}).click(function() {
			$.each(self.page.banners, function(_index, templateObject) {
				if ( templateObject.redirectsTo && !templateObject.bypassRedirect ) {
					templateObject.bypassRedirect = true;
				}
			});
			self.refresh();
		});
	}
	
	var classForAllDropdown = Dialog.makeDropdown(config.bannerDefaults.classes).change(function() {
		var newValue = $(this).val();
		$('span.rater-dialog-row-class > select').val(newValue).change();
	}).prepend($('<option>').attr('disabled','disabled').text('Class'));
	
	var importanceForAllDropdown = Dialog.makeDropdown(config.bannerDefaults.importances).change(function() {
		var newValue = $(this).val();
		$('span.rater-dialog-row-importance > select').val(newValue).change();
	}).prepend($('<option>').attr('disabled','disabled').text('Importance'));
	
	var setAll = Dialog.makeButton({
		label:	'Set all',
		icon:	'tag'
	});
	setAll.find('span.oo-ui-labelElement-label').append(
		classForAllDropdown,
		importanceForAllDropdown
	);
	
	var pageInfo = $('<div>');
	if ( self.page.oresScore ) {
		pageInfo.append(
			$('<div>').append(
				Dialog.makeIcon('ores', false),
				'&nbsp;',
				extraJs.makeLink('mw:ORES', 'ORES'),
				' Predicted class: ',
				$('<b>').text(self.page.oresScore)
			)
		);
	}
	if ( self.page.subjectRedirectsTo ) {
		pageInfo.append(
			$('<div>').append(
				Dialog.makeIcon('redirect', false),
				' Page redirects to: ',
				extraJs.makeLink(self.page.subjectRedirectsTo)
			)
		);
	}
	
	
	return $('<div>').attr('id', 'rater-dialog-actions').addClass('rater-dialog-row').append(
		pageInfo,
		$('<div>').append(
			addProject,
			removeAll,
			clearAll,
			bypassAllRedirects,
			setAll
		)
	);
};

Dialog.prototype.makeButtons = function() {
	if ( this.$footerButtons.children().length !== 0 ) {
		return;
	}
	
	var self = this;
	
	var cancel = Dialog.makeButton({
		label:	'Cancel',
		framed:	false,
		flags:	['destructive']
	}).click(function() { self.close(); });
	
	var save = Dialog.makeButton({
		label:	'Save changes',
		flags:	['primary', 'progressive']
	}).click(function() { self.onSaveClick(); });
	
	var preview = Dialog.makeButton({
		label:	'Show preview'
	}).click(function() {
		Dialog.showOverlayDialog( self.page.makePreview(), 'Preview' );
	});

	var showdiff = Dialog.makeButton({
		label:	'Show changes'
	}).click(function() {
		Dialog.showOverlayDialog( self.page.makeDiff(), 'Diff' ); 
	});
	
	self.setFooterButtons([save, preview, showdiff, cancel]);
};
Dialog.prototype.onSaveClick = function() {
	var self = this;

	var close = Dialog.makeButton({
		label:	'Close'
	}).click(function() { self.close(); });
	
	self.emptyContent();
	self.setFooterButtons(null);
	self.addToBody('Saving...');
	
	$.when( self.page.makeEdit() )
	.done( function() {
		self.addToBody('Done!');
	})
	.fail( function(code, jqxhr) {
		self.addToBody('Failed. ' + extraJs.makeErrorMsg(code, jqxhr));
	} )
	.always(function() {
		self.setFooterButtons(close);
	});
};

Dialog.prototype.autofillParams = function($rowDiv) {
	var self = this;
	var $top = $rowDiv || $('#rater-dialog-body');
	
	// Autofill empty classes (if possible)
	var extrapolateClassFromExisting = function() {
		var classes = $('span.rater-dialog-row-class > select').map(function() {
			return $(this).val();
		}).get().sort();
		if ( -1 === $.inArray(' ', classes) ) {
			return false;
		}
		return classes.filter(function(c) {
			return c!== ' ';
		})[0];
	};
	var extrapolateClassFromOres = function() {
		if ( !self.page.oresScore ) {
			return null;
		}
		return ( self.page.oresScore === 'Stub' ) ? 'Stub' : 'Start';
	};
	var extrapolated = extrapolateClassFromExisting() || extrapolateClassFromOres();
	if ( extrapolated ) {
		$top.find('span.rater-dialog-row-class > select').each(function() {
			var $this = $(this);
			if ( $this.val() === ' ' ) {
				$this.val(extrapolated).change();
				$this.parent().addClass('rater-dialog-autofill')
				.on('keypress change', function(){
					$(this).removeClass('rater-dialog-autofill').off('keypress change');
				});
			}
		});
	}
	
	// Autofill empty importances to 'low' (articles only)
	if ( config.mw.wgNamespaceNumber <= 1 && !self.page.subjectRedirectsTo ) {
		$top.find('span.rater-dialog-row-importance > select').each(function() {
			var $this = $(this);
			if ( $this.val() === ' ' ) {
				$this.val('Low').change();
				$this.parent().addClass('rater-dialog-autofill')
				.on('keypress change', function(){
					$(this).removeClass('rater-dialog-autofill').off('keypress change');
				});
			}
		});
	}
	
	// Autofill listas parameter for WP:BIO
	var wpbioBanner = self.page.getBannerFromNameOrRedirect('WikiProject Biography');
	if ( wpbioBanner && !wpbioBanner.parameters.listas ) {
		var autoListas = self.page.getListasAutofill();
		$rows = $rowDiv || $top.find('div.rater-dialog-row');		
		$wpbioRow = $rows.has('span.rater-dialog-templateName:contains("'+
			wpbioBanner.getTransclusionName() + '")');		
		$wpbioRow.find('span.rater-dialog-paraInput').has('span:contains("listas")')
		.addClass('rater-dialog-autofill')
		.on('keypress.first change.first', function() {
			$(this).removeClass('rater-dialog-autofill').off('keypress.first change.first');
		})
		.find('input').val(autoListas);
		wpbioBanner.setParamValue('listas', autoListas);
		
	}
};

Dialog.prototype.build = function(isInitialBuild) {
	var self = this;
	var isBuilt = $.Deferred();
	
	var parseBanners = '';
	if ( self.page.banners ) {
		parseBanners = self.page.banners.map(function(banner) {
			return banner.parseClassesAndImportances();
		});
	}
	
	$.when.apply(null, parseBanners).then(function() {
		if ( self.page.banners ) {
			self.addToBody(
				self.page.banners.map(function(banner) {
					if ( banner.remove ) {
						return '';
					}
					return self.makeRow(banner);
				})
			);
		}
		
		self.addToBody(self.makeActionsRow());
		
		if ( isInitialBuild && self.page.banners.length>0 ) {
			self.autofillParams();			
		}

		self.makeButtons();
		self.resetHeight();
		// Focus on Add WikiProject (first button within actions row)
		$("#rater-dialog-actions").find('span.oo-ui-buttonElement').first().find('a').focus();
		
		isBuilt.resolve();
	});
	return isBuilt;
};
	
Dialog.prototype.refresh = function() {
	this.emptyContent();
	this.build();
};

Dialog.prototype.rebuildRow = function(rowDiv, banner) {
	this.makeRow(banner).insertAfter(rowDiv);
	rowDiv.remove();
};

/* ========== ============ ====================================================================== */



/* ========== Set up current page and dialog ==================================================== */
var setupRater = function(clickEvent) {
	if ( clickEvent ) {
		clickEvent.preventDefault();
	}
	
	var currentPage = Page.newFromText(config.mw.wgPageName);
	
	var dialog = new Dialog(currentPage);
	dialog.addToBody(
		$('<div>').attr('id', 'dialog-loading').append(
			$('<p>').attr('id', 'dialog-loading-0').css('font-weight', 'bold').text('Initialising:'),
			$('<p>').attr('id', 'dialog-loading-1').text('Loading talkpage wikitext...'),
			$('<p>').attr('id', 'dialog-loading-2').text('Parsing talkpage templates...'),
			$('<p>').attr('id', 'dialog-loading-3').text('Checking if page redirects...'),
			$('<p>').attr('id', 'dialog-loading-4').text('Retrieving quality prediction...').hide(),
			$('<p>').attr('id', 'dialog-loading-5').text('Building interface...')
		)
	);
	
	var showTaskDone = function(taskNumber) {
		$('#dialog-loading-'+taskNumber).append(' Done!');
	};
	var showTaskFailed = function(taskNumber, code, jqxhr) {
		$('#dialog-loading-'+taskNumber).append(
			' Failed.',
			( code == null ) ? '' : ' ' + extraJs.makeErrorMsg(code, jqxhr)
		);
	};

	// Load and parse talk page
	var talkDeferred = currentPage.getTalkpageTopSection()
	.done(function() { showTaskDone(1); })
	.fail(function(code, jqxhr) { showTaskFailed(1, code, jqxhr); })
	.then(function(){
		return currentPage.setTemplatesBanners()
		.done(function() { showTaskDone(2); })
		.fail(function(code, jqxhr) { showTaskFailed(2, code, jqxhr); });
	});

	// Check if page is a redirect - but don't error out if request fails
	var redirDeferred = $.Deferred();
	currentPage.getSubjectRawWikitext()
	.always(function(rawPage) { 
		if ( /^\s*#REDIRECT/i.test(rawPage) ) {
			currentPage.subjectRedirectsTo = rawPage.slice(rawPage.indexOf('[[')+2, rawPage.indexOf(']]')) || true;
		}
		showTaskDone(3);
		redirDeferred.resolve();
	});

	// Retrieve rating from ORES
	var oresDeferred = ( config.mw.wgNamespaceNumber <= 1 ) ? $.Deferred() : '';
	if ( oresDeferred ) {
		$('#dialog-loading-4').show();
		redirDeferred.always(function() {
			if ( currentPage.subjectRedirectsTo ) {
				showTaskDone(4);
				oresDeferred.resolve();
				return;
			}
			
			currentPage.getOresScore()
			.done(function() {
				showTaskDone(4);
				oresDeferred.resolve();
			})
			.fail(function(code, jqxhr) {
				showTaskFailed(4, code, jqxhr);
				oresDeferred.reject();
			});
		});
	}

	// Build dialog
	$.when(oresDeferred, redirDeferred, talkDeferred).then(function() { dialog.build(true); })
	.done(function(){
		showTaskDone(5);
		$('#dialog-loading').hide(600);
	})
	.fail(function(code, jqxhr) { showTaskFailed(5, code, jqxhr); });

};

// Add portlet link
mw.util.addPortletLink( 'p-cactions', '#', 'Rater', 'ca-rater', 'Rate quality and importance' );
$('#ca-rater').click(setupRater);



/* ==========  End of file closure wrappers ===================================================== */
});
});