User:Mvolz (WMF)/CiteToolWIP.js

From Wikidata
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 Enable the script with the following code in your common.js

mw.loader.using(['wikibase'], function() {
	$.getScript( 'https://www.wikidata.org/w/index.php?title=User:Mvolz_(WMF)/CiteTool.js&action=raw&ctype=text/javascript', function() {
		var citeTool = new wb.CiteTool( 'https://www.wikidata.org/w/index.php?title=User:Mvolz_(WMF)/CiteProperties.json&action=raw&ctype=application/json' );
		citeTool.init();
	});
});
 */

( function( wb, dv, mw, $ ) {

'use strict';

function CiteTool( configUrl ) {
	this.configUrl = configUrl;
	this.config = null;

	this.citoidClient = new mw.CitoidClient();
	this.citeToolReferenceEditor = null;
	this.citeToolAutofillLinkRenderer = null;
}

CiteTool.prototype.init = function() {
	var self = this;

	if ( !mw.config.exists( 'wbEntityId' ) ) {
		return;
	}

	$( '.wikibase-entityview' )
		.on( 'referenceviewafterstartediting', function( e ) {
			self.initAutofillLink( e.target );
		} );

	// @fixme the event also fires for other changes, like editing qualifiers
	$( '.wikibase-entityview' )
		.on( 'snakviewchange', function( e ) {
			self.initAutofillLink( e.target );
		} );

	this.pendingDialog = new mw.PendingDialog( {
	  size: 'small'
	} );

	this.windowManager = new OO.ui.WindowManager();

	$( 'body' ).append( this.windowManager.$element );

	this.windowManager.addWindows( [ this.pendingDialog ] );

};

CiteTool.prototype.getConfig = function() {
	var dfd = $.Deferred();

	$.ajax({
		url: this.configUrl,
		dataType: 'json',
		success: function( config ) {
			dfd.resolve( config );
		},
		error: function( result ) {
			console.log( 'Error loading citoid config' );
		}
	});

	return dfd.promise();
};

CiteTool.prototype.initAutofillLink = function( target ) {
	var self = this;

	if ( this.config === null ) {
		this.getConfig()
			.done( function( config ) {
				self.config = config;
				self.citeToolReferenceEditor = new wb.CiteToolReferenceEditor( config, self.windowManager, self.pendingDialog);
				self.citeToolAutofillLinkRenderer = new wb.CiteToolAutofillLinkRenderer(
					config,
					self.citoidClient,
					self.citeToolReferenceEditor,
					self.windowManager,
					self.pendingDialog
				);

				self.checkReferenceAndAddAutofillLink( target );
			} );
	} else {
		var refViews = $( target ).closest( '.wikibase-referenceview' );
		self.checkReferenceAndAddAutofillLink( refViews[0] );
	}
};

CiteTool.prototype.checkReferenceAndAddAutofillLink = function( target ) {
	if ( $( target ).find( '.wikibase-citetool-autofill' ).length > 0 ) {
		return;
	}

	var reference = this.getReferenceFromView( target );

	if ( !reference ) {
		return;
	}

	this.citeToolAutofillLinkRenderer.renderLink( target );

	if ( this.getLookupSnakProperty( reference ) !== null ) {
		// Disable for invalid properties
		this.citeToolAutofillLinkRenderer.disableLink( target );
	} else {
		// Enable if property is valid
		this.citeToolAutofillLinkRenderer.enableLink( target );
	}
};

CiteTool.prototype.getReferenceFromView = function( referenceView ) {
	// not a reference view change
	if ( referenceView === undefined ) {
		return null;
	}

	var refView = $( referenceView ).data( 'referenceview' );

	return refView.value();
};

CiteTool.prototype.getLookupSnakProperty = function( reference ) {
	var snaks = reference.getSnaks(),
		lookupProperties = this.getLookupProperties(),
		lookupProperty = null;

	snaks.each( function( k, snak ) {
		var propertyId = snak.getPropertyId();

		if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
			if ( lookupProperty === null ) {
				lookupProperty = propertyId;
			}
		}
	} );

	return lookupProperty;
};

CiteTool.prototype.getLookupProperties = function() {
	var properties = [];

	if ( this.config.properties ) {
		properties = Object.keys( this.config.properties );
	}

	return properties;
};

wb.CiteTool = CiteTool;

}( wikibase, dataValues, mediaWiki, jQuery ) );

( function( wb, dv, mw, $ ) {

'use strict';

function CiteToolAutofillLinkRenderer( config, citoidClient, citeToolReferenceEditor, windowManager, pendingDialog ) {
	this.config = config;
	this.citoidClient = citoidClient;
	this.citeToolReferenceEditor = citeToolReferenceEditor;
	this.windowManager = windowManager;
	this.pendingDialog = pendingDialog;
}

CiteToolAutofillLinkRenderer.prototype.renderLink = function( referenceView ) {
	var self = this;

	var options = {
		templateParams: [
			'wikibase-citetool-autofill wikibase-referenceview-autofill', // CSS class names
			'#', // URL
			'autofill', // Label
			'' // Title tooltip
		],
		cssClassSuffix: 'autofill'
	};

	var $span = $( '<span/>' )
		.toolbarbutton( options )
		.on( 'click', function( e ) {
			e.preventDefault();
			self.onAutofillClick( e.target );
		} );

	this.getReferenceToolbarContainer( referenceView ).append( $span );
};

CiteToolAutofillLinkRenderer.prototype.disableLink = function( target ){
	var buttons = $( target ).find( '.wikibase-citetool-autofill' );
	var button;

	if ( buttons.length > 0 ){
		button = buttons[0];
		button.toolbarbutton.disable();
	}
};

CiteToolAutofillLinkRenderer.prototype.enableLink = function( target ){
	var buttons = $( target ).find( '.wikibase-citetool-autofill' );
	var button;

	if ( buttons.length > 0 ){
		button = buttons[0];
		button.toolbarbutton.enable();
	}
};

CiteToolAutofillLinkRenderer.prototype.getReferenceFromView = function( referenceView ) {
	// not a reference view change
	if ( referenceView === undefined ) {
		return null;
	}

	var refView = $( referenceView ).data( 'referenceview' );

	return refView.value();
};

CiteToolAutofillLinkRenderer.prototype.getLookupSnakProperty = function( reference ) {
	var snaks = reference.getSnaks(),
		lookupProperties = this.getLookupProperties(),
		lookupProperty = null;

	snaks.each( function( k, snak ) {
		var propertyId = snak.getPropertyId();

		if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
			if ( lookupProperty === null ) {
				lookupProperty = propertyId;
			}
		}
	} );

	return lookupProperty;
};

CiteToolAutofillLinkRenderer.prototype.getLookupProperties = function() {
	var properties = [];

	if ( this.config.properties ) {
		properties = Object.keys( this.config.properties );
	}

	return properties;
};

CiteToolAutofillLinkRenderer.prototype.getReferenceToolbarContainer = function( referenceView ) {
	var $heading = $( referenceView ).find( '.wikibase-referenceview-heading' ),
		$toolbar = $heading.find( '.wikibase-toolbar-container' );

	return $toolbar;
};

CiteToolAutofillLinkRenderer.prototype.onAutofillClick = function( target ) {
	var referenceView = $( target ).closest( '.wikibase-referenceview' ),
		reference = this.getReferenceFromView( referenceView ),
		self = this;

	if ( reference === null ) {
		return;
	}

	var value = this.getLookupSnakValue( reference );

	self.windowManager.openWindow( self.pendingDialog );
	self.pendingDialog.pushPending();
	self.pendingDialog.getActionProcess.call( self.pendingDialog, 'waiting' );

	this.citoidClient.search( value )
		.then(
			//success
			function( data ) {
				if ( data[0] ) {
					self.citeToolReferenceEditor.addReferenceSnaksFromCitoidData(
						data[0],
						referenceView
					);
				}
			},
			//failure
			function() {
				self.pendingDialog.popPending();
				self.pendingDialog.getActionProcess.call( self.pendingDialog, 'error' );
			}

		);
};

CiteToolAutofillLinkRenderer.prototype.getLookupSnakValue = function( reference ) {
	var value = null,
		lookupProperties = this.getLookupProperties();

	reference.getSnaks().each( function( k, snak ) {
		var propertyId = snak.getPropertyId();

		if ( lookupProperties.indexOf( propertyId ) !== -1 ) {
			value = snak.getValue().getValue();
		}
	} );

	return value;
};

wb.CiteToolAutofillLinkRenderer = CiteToolAutofillLinkRenderer;

}( wikibase, dataValues, mediaWiki, jQuery ) );

( function( wb, dv, mw, $ ) {

'use strict';

function CiteToolReferenceEditor( config, windowManager, pendingDialog ) {
	this.config = config;
	this.windowManager = windowManager;
	this.pendingDialog = pendingDialog;

	this.citoidClient = new mw.CitoidClient();
	this.openRefineClient = new mw.OpenRefineClient();
	this.sparql = new wb.queryService.api.Sparql();
	this.lc = new mw.LanguageConverter();
}

CiteToolReferenceEditor.prototype.addReferenceSnaksFromCitoidData = function( data, referenceView ) {

	var publicationTypes = [
		'Q5633421',
		'Q41298',
		'Q1110794',
		'Q35127',
		'Q32635',
		"Q838948",
		"Q5057302",
		"Q686822",
		"Q17928402",
		"Q571",
		"Q3331189",
		"Q1980247",
		"Q2334719",
		"Q5057302",
		"Q23927052",
		"Q4423781",
		"Q49848",
		"Q9158",
		"Q17329259",
		"Q11424",
		"Q7216866",
		"Q545861",
		"Q30070565",
		"Q178651",
		"Q191067",
		"Q133492",
		"Q191067",
		"Q87167",
		"Q4006",
		"Q191067",
		"Q253623",
		"Q5057302",
		"Q604733",
		"Q1555508",
		"Q10870555",
		"Q820655",
		"Q1266946",
		"Q21191270",
		"Q30070675",
		"Q36774"
	];

	var statedIn = "P248";

	var refView = $( referenceView ).data( 'referenceview' ),
		lv = this.getReferenceSnakListView( refView ),
		// usedProperties = refView.value().getSnaks().getPropertyOrder(),
		self = this;

	var addedSnakItem = false;
	var snakPromises = [];

	// Try to add "stated in" statement by search via doi
	var doiProm = self.openRefineClient.search( data.DOI ).then( function ( results ) {
		console.log('doi');
		if ( results.result[0] ) {
			lv.addItem(
				self.getWikibaseItemSnak( statedIn, results.result[0].id )
			);
			addedSnakItem = true;
		}
	});

	snakPromises.push(doiProm);

	$.each( data, function( key, val ) {
		var propertyId = self.getPropertyForCitoidData( key );

		// Some default values for an openRefine query
		var query = {
			query : val,
			limit : 1,
			type : null,
			type_strict : "any"
		};

		// Method used for adding item snaks with OpenRefine
		function addItemSnak( q ) {
			return self.openRefineClient.search( q ).then( function ( results ) {
				console.log( propertyId );
				if ( results.result[0] ) {
					lv.addItem(
						self.getWikibaseItemSnak( propertyId, results.result[0].id )
					);
					addedSnakItem = true;
				}
			});
		}

		// var properties = [
		// 	{ p : self.getPropertyForCitoidData("DOI"), v : data.DOI }
		//	];

		// var properties = [
		// 	{ p : self.getPropertyForCitoidData("ISSN"), v : data.ISSN }
		//	];

		if ( !propertyId ) {
			console.log( "PropertyID missing for key: " + key );
			return;
		}

		// Allow duplicate properties; i.e. multiple authors
		// TODO: Exclude identical snaks with same property and value
		// if ( propertyId !== null && usedProperties.indexOf( propertyId ) !== -1 ) {
		// 	return;
		// }

		switch ( key ) {
			// Monolingual properties
			case 'title':
				lv.addItem( self.getMonolingualValueSnak(
					propertyId,
					val,
					self.getTitleLanguage( data )
				) );
				addedSnakItem = true;

				break;
			// Date properties
			case 'date':
			case 'accessDate':
				try {
					lv.addItem(
						self.getDateSnak( propertyId, val )
					);

					addedSnakItem = true;
				} catch ( e ) {
					console.log( e );
				}

				break;
			// Item properties
			case 'language':
				query.type = ['Q1288568', 'Q33742', 'Q951873', 'Q33384', 'Q34770', 'Q1002697']; // Modern language, natural language, language, ethnolect, dialect
				snakPromises.push( addItemSnak( query ) );
				break;
			case 'publicationTitle':
				//query.type = null; // Too many to enumerate fully, do by score instead?
				query.type = publicationTypes;
				snakPromises.push( addItemSnak( query ) );

				lv.addItem( self.getMonolingualValueSnak(
					'P6333',
					val,
					self.getTitleLanguage( data )
				) );
				addedSnakItem = true;
				break;
			case 'publisher':
				query.type = ['Q2085381', 'Q479716', 'Q1114515', 'Q149985', 'Q748822', 'Q18127', 'Q2024496', 'Q327333'];
				snakPromises.push( addItemSnak( query ) );
				break;
			// number properties
			case 'numPages':
				break;
			// String properties
			case 'author':
			case 'contributor':
			case 'editor':
			case 'translator':
			case 'seriesEditor':
			case 'interviewee':
			case 'interviewer':
			case 'director':
			case 'scriptwriter':
			case 'producer':
			case 'castMember':
			case 'sponsor':
			case 'counsel':
			case 'inventor':
			case 'attorneyAgent':
			case 'recipient':
			case 'performer':
			case 'composer':
			case 'wordsBy':
			case 'cartographer':
			case 'programmer':
			case 'artist':
			case 'commenter':
			case 'presenter':
			case 'guest':
			case 'podcaster':
			case 'reviewedAuthor':
			case 'cosponsor':
			case 'bookAuthor':
				for (var i = 0; i < val.length; i++) {
					try {
						lv.addItem(
							self.getStringSnak( propertyId, val[i][0] + " " + val[i][1] )
						);
						addedSnakItem = true;
					}
					catch( e ) {
						console.log(e);
					}
				}
				break;
			case 'ISSN':
			case 'ISBN':
			case 'PMID':
			case 'url':
			case 'pages':
			case 'issue':
			case 'volume':
			case 'PMCID':
			case 'DOI':
				var str = false;
				if (typeof val === 'string') {
					str = true;
					try {
						lv.addItem(
							self.getStringSnak( propertyId, val )
						);
						addedSnakItem = true;
					}
					catch( e ) {
						console.log(e);
					}
				// For array of identifiers, add every one
				} else if ( Array.isArray( val ) ) {
					for (i = 0; i < val.length; i++) {
						try {
							lv.addItem(
								self.getStringSnak( propertyId, val[i] )
							);
							addedSnakItem = true;
						}
						catch( e ) {
							console.log(e);
						}
					}
				}

				// Below currently does not work
				//var queryProperty = self.getQueryPropertyForCitoidData( key );

				// Very hacky - should try every id in Array instead of just first one
				//if (!str) {
				//	val = val[0];
				//}
				// self.lookupItemByIdentifier( queryProperty, val )
				// 	.done( function( itemId ) {
				// 		console.log( itemId );
				// 		console.log( propertyId );
				// 		try {
				// 			if (itemId && propertyId) {
				// 				lv.addItem(
				// 					self.getWikibaseItemSnak( propertyId, itemId )
				// 				);
				// 				addedSnakItem = true;
				// 			}
				// 		} catch( e ) {}
				// 	} );

				break;
			default:
				break;
		}
	} );

	if ( addedSnakItem === true ) {

		$.when.apply( $, snakPromises ).then( function() {
			lv.startEditing().then( function() {
				self.pendingDialog.popPending();
				self.windowManager.closeWindow( self.pendingDialog );
			});
		});
	}
};

CiteToolReferenceEditor.prototype.getReferenceSnakListView = function( refView ) {
	var refListView = refView.$listview.data( 'listview' ),
		snakListView = refListView.items(),
		snakListViewData = snakListView.data( 'snaklistview' ),
		listView = snakListViewData.$listview.data( 'listview' );

	return listView;
};

CiteToolReferenceEditor.prototype.getPropertyForCitoidData = function( key ) {
	if ( this.config.zoteroProperties[key] ) {
		return this.config.zoteroProperties[key];
	}

	return null;
};

CiteToolReferenceEditor.prototype.getQueryPropertyForCitoidData = function( key ) {
	if ( this.config.queryProperties[key] ) {
		return this.config.queryProperties[key];
	}

	return null;
};

CiteToolReferenceEditor.prototype.getTitleLanguage = function( data ) {

	return this.lc.getMonolingualCode(data.language);
};

CiteToolReferenceEditor.prototype.getMonolingualValueSnak = function( propertyId, title, languageCode ) {
	return new wb.datamodel.PropertyValueSnak(
		propertyId,
		new dv.MonolingualTextValue( languageCode, title )
	);
};

CiteToolReferenceEditor.prototype.getStringSnak = function( propertyId, val) {
	return new wb.datamodel.PropertyValueSnak(
		propertyId,
		new dv.StringValue( val )
	);
};

CiteToolReferenceEditor.prototype.getNumSnak = function( propertyId, val) {
	return new wb.datamodel.NumberValueSnak(
		propertyId,
		new dv.StringValue( val )
	);
};

CiteToolReferenceEditor.prototype.getDateSnak = function( propertyId, dateString ) {
	var timestamp = dateString + 'T00:00:00Z';

	return new wb.datamodel.PropertyValueSnak(
		propertyId,
		new dv.TimeValue( timestamp )
	);
};

CiteToolReferenceEditor.prototype.getWikibaseItemSnak = function( propertyId, itemId ) {
	return new wb.datamodel.PropertyValueSnak(
		propertyId,
		new wb.datamodel.EntityId( itemId )
	);
};

CiteToolReferenceEditor.prototype.lookupItemByIdentifier = function( propertyId, value ) {
	var query = "SELECT ?identifier ?entity "
		+ "WHERE {  ?entity wdt:" + propertyId + " ?identifier . "
		+ "FILTER ( ?identifier in ('" + value + "') ) "
		+ "}";

	var dfd = $.Deferred(),
		baseUrl = 'https://query.wikidata.org/bigdata/namespace/wdq/sparql';

	$.ajax( {
		method: 'GET',
		url: baseUrl,
		data: {
			query: query,
			format: 'json'
		}
	} )
	.done( function( data ) {
		var uriPattern = /^http:\/\/www.wikidata.org\/entity\/([PQ]\d+)$/;

		if ( data.results.bindings.length > 0 ) {
			var result = data.results.bindings[0],
				uri = result.entity.value,
				matches = uri.match(uriPattern);

			dfd.resolve( matches[1] );
		} else {
			dfd.resolve( false );
		}
	} );

	return dfd.promise();
};

wb.CiteToolReferenceEditor = CiteToolReferenceEditor;

}( wikibase, dataValues, mediaWiki, jQuery ) );

( function( mw, $ ) {

'use strict';

function CitoidClient() {

}

CitoidClient.prototype.search = function( value ) {
    var baseUrl = 'https://en.wikipedia.org/api/rest_v1/data/citation',
        format = 'mediawiki-basefields',
        url = baseUrl + '/' + format + '/' + encodeURIComponent(value);

    return $.ajax( {
        method: 'GET',
        url: url
    } );
};

mw.CitoidClient = CitoidClient;

}( mediaWiki, jQuery ) );

( function( mw, $ ) {

'use strict';

function LanguageConverter() {

	this.monolingualCodes= null;
	this.contentCodes = null;
	this.ready = false;

}

LanguageConverter.prototype.init = function () {

	this.service = new mw.Api();
	const self = this;

	// Cache multilingual codes
	this.service
		.get( {
			action: "query",
			format: "json",
			meta: "wbcontentlanguages",
			formatversion: "2",
			wbclcontext: "monolingualtext"
		} )
		.then ( function ( results ) {
			if ( results && results.query && results.query.wbcontentlanguages  ) {
				self.monolingualCodes = results.query.wbcontentlanguages;
			}
		} );

	// Cache content language codes
	this.service
		.get( {
			action: "query",
			format: "json",
			meta: "wbcontentlanguages",
			formatversion: "2",
			wbclcontext: "term"
		} )
		.then ( function ( results ) {
			if ( results && results.query && results.query.wbcontentlanguages ) {
				self.contentCodes = results.query.wbcontentlanguages;
			}
		} );
};

// Get IETF language code root as fallback
const getCodeFallback = function ( code ) {
	code = code.split( "-" );
	code = code[0];
	return code;
};

// Common to getContentCode and getMonolingualCode
const getCode = function( value, cachedCodes, defaultCode ) {
	if ( !value ) {
		return defaultCode;
	}

	value = value.toLowerCase();
	let code = this.cachedCodes[ value ];

	if ( !code ) {
		code = getCodeFallback( value );
		code = this.cachedCodes[ code ];
		if ( !code ) {
			return defaultCode;
		}
	}
	code = code.code;
	return code;
};

// Get valid codes for multilingual text
LanguageConverter.prototype.getMonolingualCode = function( value ) {
	return getCode( value, this.monolingualCodes, 'und');
};

// Get valid codes for content languages i.e. labels and desc
LanguageConverter.prototype.getContentCode = function( value ) {
	return getCode( value, this.contentCodes, 'en');
};

LanguageConverter.prototype.getQID = function( value ) {
	// fallback
	return false;
};

mw.LanguageConverter = LanguageConverter;

}( mediaWiki, jQuery ) );


( function( mw, $ ) {

'use strict';

function OpenRefineClient() {

}

OpenRefineClient.prototype.search = function( query ) {
	var url = 'https://tools.wmflabs.org/openrefine-wikidata/en/api';
	if ( typeof query === 'string' ){
		return $.ajax( {
			method: 'GET',
			url: url,
			crossDomain: true,
			data: {
				query: query
			}
		} );
	} else {
		return $.ajax( {
			method: 'GET',
			url: url,
			crossDomain: true,
			data: {
				query: JSON.stringify(query)
			}
		} );
	}
};

mw.OpenRefineClient = OpenRefineClient;

}( mediaWiki, jQuery ) );

( function( mw, $ ) {

'use strict';

function PendingDialog( config ) {
	PendingDialog.super.call( this, config );
}
OO.inheritClass( PendingDialog, OO.ui.ProcessDialog );

// Specify a name for .addWindows()
PendingDialog.static.name = 'PendingDialog';
// Specify a title statically (or, alternatively, with data passed to the opening() method).
PendingDialog.static.title = 'Generating Citation';

PendingDialog.static.actions = [
	{ action: 'waiting', modes: 'edit', label: 'Searching' },
	{ action: 'error', modes: 'edit', label: 'Error' }
];

// Customize the initialize() function: This is where to add content to the dialog body and set up event handlers.
PendingDialog.prototype.initialize = function () {
	// Call the parent method
	PendingDialog.super.prototype.initialize.apply( this, arguments );
	// Create and append a layout and some content.

	this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	this.panel1.$element.append( '<p>Generating a reference for you.... please wait...</p>' );
	this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	this.panel2.$element.append( '<p>Unable to generate reference from this input. Press escape to exit.</p>' );

	this.stackLayout= new OO.ui.StackLayout( {
	items: [ this.panel1, this.panel2 ]
	} );

	this.stackLayout.setItem( this.panel1 );
	this.$body.append( this.stackLayout.$element );

};

PendingDialog.prototype.getSetupProcess = function ( data ) {
	return PendingDialog.super.prototype.getSetupProcess.call( this, data )
	.next( function () {
	this.actions.setMode( 'waiting' );
	}, this );
};


// Specify processes to handle the actions.
PendingDialog.prototype.getActionProcess = function ( action ) {
	if ( action === 'waiting' ) {
	//this.pushPending();
	// Set the mode to help.
	this.actions.setMode( 'waiting' );
	// Show the help panel.
	this.stackLayout.setItem( this.panel1 );
	}
	if ( action === 'error' ) {
	//this.popPending();
	// Set the mode to help.
	this.actions.setMode( 'error' );
	// Show the help panel.
	this.stackLayout.setItem( this.panel2 );
	}
	// Fallback to parent handler
	return PendingDialog.super.prototype.getActionProcess.call( this, action );
};


// Override the getBodyHeight() method to specify a custom height (or don't to use the automatically generated height)
PendingDialog.prototype.getBodyHeight = function () {
	return this.panel1.$element.outerHeight( true );
};

mw.PendingDialog = PendingDialog;

}( mediaWiki, jQuery ) );

var wikibase = wikibase || {};
wikibase.queryService = wikibase.queryService || {};
wikibase.queryService.api = wikibase.queryService.api || {};

wikibase.queryService.api.Sparql = ( function( $ ) {
	'use strict';

	var SPARQL_SERVICE_URI = 'https://query.wikidata.org/bigdata/namespace/wdq/sparql';

	var ERROR_CODES = {
			TIMEOUT: 10,
			MALFORMED: 20,
			SERVER: 30,
			UNKNOWN: 100
	};

	var ERROR_MAP = {
		'QueryTimeoutException: Query deadline is expired': ERROR_CODES.TIMEOUT,
		'MalformedQueryException: ': ERROR_CODES.MALFORMED
	};

	/**
	 * SPARQL API for the Wikibase query service
	 *
	 * @class wikibase.queryService.api.Sparql
	 * @license GNU GPL v2+
	 *
	 * @author Stanislav Malyshev
	 * @author Jonas Kress
	 * @constructor
	 *
	 * @param {string} [serviceUri] Optional URI to the SPARQL service endpoint
	 */
	function SELF( serviceUri ) {
		this._serviceUri = serviceUri || SPARQL_SERVICE_URI;
	}

	/**
	 * @property {Object}
	 */
	SELF.prototype.ERROR_CODES = ERROR_CODES;

	/**
	 * @property {Number}
	 * @private
	 */
	SELF.prototype._serviceUri = null;

	/**
	 * @property {Number}
	 * @private
	 */
	SELF.prototype._executionTime = null;

	/**
	 * @property {Object}
	 * @private
	 */
	SELF.prototype._error = null;

	/**
	 * @property {Number}
	 * @private
	 */
	SELF.prototype._resultLength = null;

	/**
	 * @property {Object}
	 * @private
	 */
	SELF.prototype._rawData = null;

	/**
	 * @property {string}
	 * @private
	 */
	SELF.prototype._queryUri = null;

	/**
	 * Submit a query to the API
	 *
	 * @return {jQuery.Promise}
	 */
	SELF.prototype.queryDataUpdatedTime = function() {
		// Cache the update time only for a minute
		var deferred = $.Deferred(), query = encodeURI( 'prefix schema: <http://schema.org/> ' +
				'SELECT * WHERE {<http://www.wikidata.org> schema:dateModified ?y}' ), url = this._serviceUri +
				'?query=' + query + '&nocache=' + Math.floor( Date.now() / 60000 ), settings = {
			headers: {
				Accept: 'application/sparql-results+json'
			}
		};

		$.ajax( url, settings )
			.done(
					function( data, textStatus, jqXHR ) {
						if ( !data.results.bindings[0] ) {
							deferred.reject();
							return;
						}
						var updateDate = new Date(
								data.results.bindings[0][data.head.vars[0]].value ), dateText = updateDate
								.toLocaleTimeString( navigator.language, {
									timeZoneName: 'short'
								} ) +
								', ' + updateDate.toLocaleDateString( navigator.language, {
									month: 'short',
									day: 'numeric',
									year: 'numeric'
								} );
							var differenceInSeconds = Math
									.round( ( new Date() - updateDate ) / 1000 );

						deferred.resolve( dateText, differenceInSeconds );
					} ).fail( function() {
				deferred.reject();
			} );

		return deferred;
	};

	/**
	 * Submit a query to the API
	 *
	 * @param {string[]} query
	 * @return {jQuery.Promise} query
	 */
	SELF.prototype.query = function( query ) {
		var self = this, deferred = $.Deferred(), settings = {
			headers: {
				Accept: 'application/sparql-results+json'
			}
		};

		this._queryUri = this._serviceUri + '?query=' + encodeURIComponent( query );

		this._executionTime = Date.now();
		$.ajax( this._queryUri, settings ).done( function( data, textStatus, request ) {
			self._executionTime = Date.now() - self._executionTime;

			if ( typeof data.boolean === 'boolean' ) {
				self._resultLength = 1;
			} else {
				self._resultLength = data.results.bindings.length || 0;
			}
			self._rawData = data;

			deferred.resolve();
		} ).fail( function( request, options, exception ) {
			self._executionTime = null;
			self._rawData = null;
			self._resultLength = null;
			self._generateErrorMessage( request, options, exception );

			deferred.reject();
		} );

		return deferred;
	};

	/**
	 * Get execution time in ms of the submitted query
	 */
	SELF.prototype._generateErrorMessage = function( request, options, exception ) {
		var error = {
			code: ERROR_CODES.UNKNOWN,
			message: null,
			debug: request.responseText
		};

		if ( request.status === 0 || exception ) {
			error.code = ERROR_CODES.SERVER;
			error.message = exception.message;
		}

		try {//extract error from server response
			var errorToMatch = error.debug.substring( error.debug
					.indexOf( 'java.util.concurrent.ExecutionException:' ) );

			for ( var errorKey in ERROR_MAP ) {
				if ( errorToMatch.indexOf( errorKey ) !== -1 ) {
					error.code = ERROR_MAP[ errorKey ];
					error.message = null;
				}
			}

			if ( error.code === ERROR_CODES.UNKNOWN || error.code === ERROR_CODES.MALFORMED ) {
				error.message = error.debug
						.match(
								/(java\.util\.concurrent\.ExecutionException\:)+(.*)(Exception\:)+(.*)/ )
						.pop().trim();
			}

		} catch ( e ) {
		}

		this._error = error;
	};

	/**
	 * Get execution time in seconds of the submitted query
	 *
	 * @return {Number}
	 */
	SELF.prototype.getExecutionTime = function() {
		return this._executionTime;
	};

	/**
	 * Get error of the submitted query if it has failed
	 *
	 * @return {object}
	 */
	SELF.prototype.getError = function() {
		return this._error;
	};

	/**
	 * Get result length of the submitted query if it has failed
	 *
	 * @return {Number}
	 */
	SELF.prototype.getResultLength = function() {
		return this._resultLength;
	};

	/**
	 * Get query URI
	 *
	 * @return {string}
	 */
	SELF.prototype.getQueryUri = function() {
		return this._queryUri;
	};

	/**
	 * Process SPARQL query result.
	 *
	 * @param {Object} data
	 * @param {Function} rowHandler
	 * @param {*} context
	 * @private
	 * @return {*} The provided context, modified by the rowHandler.
	 */
	SELF.prototype._processData = function( data, rowHandler, context ) {
		var results = data.results.bindings.length;
		for ( var i = 0; i < results; i++ ) {
			var rowBindings = {};
			for ( var j = 0; j < data.head.vars.length; j++ ) {
				if ( data.head.vars[j] in data.results.bindings[i] ) {
					rowBindings[data.head.vars[j]] = data.results.bindings[i][data.head.vars[j]];
				} else {
					rowBindings[data.head.vars[j]] = undefined;
				}
			}
			context = rowHandler( rowBindings, context );
		}
		return context;
	};

	/**
	 * Encode string as CSV.
	 *
	 * @param {string} string
	 * @return {string}
	 */
	SELF.prototype._encodeCsv = function( string ) {
		var result = string.replace( /"/g, '""' );
		if ( /[",\n]/.test( result ) ) {
			result = '"' + result + '"';
		}
		return result;
	};

	/**
	 * Get the raw result
	 *
	 * @return {Object} result
	 */
	SELF.prototype.getResultRawData = function() {
		return this._rawData;
	};

	/**
	 * Get the result of the submitted query as CSV
	 *
	 * @return {string} csv
	 */
	SELF.prototype.getResultAsCsv = function() {
		var self = this,
			data = self._rawData,
			output = data.head.vars.map( this._encodeCsv ).join( ',' ) + '\n';

		output = this._processData( data, function( row, out ) {
			var rowOut = '';
			var first = true;
			var rowCSV;
			for ( var rowVar in row ) {
				if ( row[rowVar] === undefined ) {
					rowCSV = '';
				} else {
					rowCSV = self._encodeCsv( row[rowVar].value );
				}
				if ( !first ) {
					rowOut += ',';
				} else {
					first = false;
				}
				rowOut += rowCSV;
			}
			rowOut += '\n';
			return out + rowOut;
		}, output );
		return output;
	};

	/**
	 * Get the result of the submitted query as JSON
	 *
	 * @return {string}
	 */
	SELF.prototype.getResultAsJson = function() {
		var output = [],
			data = this._rawData;

		output = this._processData( data, function( row, out ) {
			var extractRow = {};
			for ( var rowVar in row ) {
				extractRow[rowVar] = ( row[rowVar] || {} ).value;
			}
			out.push( extractRow );
			return out;
		}, output );
		return JSON.stringify( output );
	};

	/**
	 * Get the result of the submitted query as raw JSON
	 *
	 * @return {string}
	 */
	SELF.prototype.getResultAsAllJson = function() {
		return JSON.stringify( this._rawData );
	};

	/**
	 * Render value as per http://www.w3.org/TR/sparql11-results-csv-tsv/#tsv
	 *
	 * @param {Object} binding
	 * @return {string}
	 */
	SELF.prototype._renderValueTSV = function( binding ) {
		var value = binding.value.replace( /\t/g, '' );
		switch ( binding.type ) {
		case 'uri':
			return '<' + value + '>';
		case 'bnode':
			return '_:' + value;
		case 'literal':
			var lvalue = JSON.stringify( value );
			if ( binding['xml:lang'] ) {
				return lvalue + '@' + binding['xml:lang'];
			}
			if ( binding.datatype ) {
				if ( binding.datatype === 'http://www.w3.org/2001/XMLSchema#integer' ||
						binding.datatype === 'http://www.w3.org/2001/XMLSchema#decimal' ||
						binding.datatype === 'http://www.w3.org/2001/XMLSchema#double' ) {
					return value;
				}
				return lvalue + '^^<' + binding.datatype + '>';
			}
			return lvalue;
		}
		return value;
	};

	/**
	 * Get the result of the submitted query as SPARQL TSV
	 *
	 * @return {string}
	 */
	SELF.prototype.getSparqlTsv = function() {
		var self = this,
			data = this._rawData,
			output = data.head.vars.map( function( vname ) {
			return '?' + vname;
		} ).join( '\t' ) + '\n';

		output = this._processData( data, function( row, out ) {
			var rowOut = '';
			var first = true;
			var rowTSV;
			for ( var rowVar in row ) {
				if ( row[rowVar] === undefined ) {
					rowTSV = '';
				} else {
					rowTSV = self._renderValueTSV( row[rowVar] );
				}
				if ( !first ) {
					rowOut += '\t';
				} else {
					first = false;
				}
				rowOut += rowTSV;
			}
			rowOut += '\n';
			return out + rowOut;
		}, output );
		return output;
	};

	/**
	 * Get the result of the submitted query as simplified TSV
	 *
	 * @return {string}
	 */
	SELF.prototype.getSimpleTsv = function() {
		var data = this._rawData,
			output = data.head.vars.join( '\t' ) + '\n';

		output = this._processData( data, function( row, out ) {
			var rowOut = '';
			var first = true;
			var rowTSV;
			for ( var rowVar in row ) {
				if ( row[rowVar] === undefined ) {
					rowTSV = '';
				} else {
					rowTSV = row[rowVar].value.replace( /\t/g, '' );
				}
				if ( !first ) {
					rowOut += '\t';
				} else {
					first = false;
				}
				rowOut += rowTSV;
			}
			rowOut += '\n';
			return out + rowOut;
		}, output );
		return output;
	};

	return SELF;

}( jQuery ) );