}}


local p = {};

local i18nDefaultLanguage = 'Q7737';
local i18nEditors = {
	Q150	= '',			-- fransuzcha
	Q1321	= '',			-- ispancha
	Q1860	= '',			-- inglizcha
	Q7737	= 'под ред. ',	-- ruscha
	Q188	= 'Hrsg.: ',		-- olmoncha
}
local i18nVolume = {
	Q150	= 'Vol.',	-- French
	Q1321	= 'Vol.',	-- Spanish
	Q1860	= 'Vol.',	-- English
	Q7737	= 'Т.',		-- Russian
}
local i18nPage = {
	Q150 = 'P.',	-- French
	Q188 = 'S.',	-- German
	Q1321 = 'P.',	-- Spanish
	Q1860 = 'P.',	-- English
	Q7737 = 'С.',	-- Russian
}

local PREFIX_CITEREF = "CITEREF_";

function p.deepcopy(orig)
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        copy = {}
        for orig_key, orig_value in pairs(orig) do
            copy[orig_key] = p.deepcopy( orig_value );
        end
	else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end

local options_commas = { separator = ', ', conjunction = ', ', format = function( src ) return src end, nolinks = false, preferids = false };
local options_commas_nolinks = { separator = ', ', conjunction = ', ', format = function( src ) return src end, nolinks = true, preferids = false };
local options_commas_it = { separator = ', ', conjunction = ', ', format = function( src ) return "''" .. src .. "''" end, nolinks = false, preferids = false };
local options_commas_it_nolinks = { separator = ', ', conjunction = ', ', format = function( src ) return "''" .. src .. "''" end, nolinks = true , preferids = false };
local options_citetypes = { separator = ' ', conjunction = ' ', format = function( src ) return 'citetype_' .. src end, nolinks = true , preferids = true };

function renderSource( src )
	mw.logObject( src );

	if ( src.code and not src.url ) then
		src.url = mw.wikibase.sitelink( src.code ) or ( 'd:' .. src.code )
		src.url = ':' .. src.url;
	end
	src.lang = getSingle( src.lang ) or i18nDefaultLanguage;
	src.title = src.title or '\'\'(unspecified title)\'\''

	if ( not src.year and src.date ) then
		local date = getSingle( src.date );
		src.year = mw.ustring.sub( date, 9, 12 );
	end

	local result = '';
	if ( src.authors ) then
		result = result .. toString( src.authors, options_commas_it );
	end
	if ( string.len( result ) ~= 0 ) then
		result = result .. ' ';
	end
 
 	if ( src.part ) then
 		if ( src.url ) then
			local url = getSingle( src.url );
			if ( string.sub( url, 1, 1 ) == ':' ) then
				result = result .. '[[' .. url .. '|' .. toString( src.part, options_commas_nolinks ) .. ']]';
			else
				result = result .. '[' .. url .. ' ' .. toString( src.part, options_commas_nolinks ) .. ']';
			end
		end
		result = result .. ' // ' .. toString( src.title, options_commas );
	else
		-- title only
 		if ( src.url ) then
			local url = getSingle( src.url );
			if ( string.sub( url, 1, 1 ) == ':' ) then
				result = result .. '[[' .. url .. '|' .. toString( src.title, options_commas_nolinks ) .. ']]';
			else
				result = result .. '[' .. url .. ' ' .. toString( src.title, options_commas_nolinks ) .. ']';
			end
		end
 	end

	if ( src.originaltitle ) then
		result = result .. ' = ' .. toString( src.originaltitle, options_commas );
	end

	if ( src.publication ) then
		result = result .. ' // ' .. toString( src.publication, options_commas_it );
	end

	if ( src.editor ) then
		local prefix = i18nEditors[ src.lang ] or i18nEditors[ i18nDefaultLanguage ];
		result = result .. ' / ' .. prefix .. toString( src.editor, options_commas );
	end

	if ( src.place or src.publisher or src.year ) then
		result = result .. ' — ';
		if ( src.place ) then
			result = result .. toString( src.place, options_commas );
			if ( src.publisher or src.year ) then
				result = result .. ': ';
			end
		end
		if ( src.publisher ) then
			result = result .. toString( src.publisher, options_commas );
			if ( src.year ) then
				result = result .. ', ';
			end
		end
		if ( src.year ) then
			result = result .. toString( src.year, options_commas );
		end
		result = result .. '.';
	end
 
	if ( src.volume ) then
		local letter = i18nVolume[ src.lang ] or i18nVolume[ i18nDefaultLanguage ];
		result = result .. ' — ' .. letter .. ' ' .. toString(src.volume, options_commas ) .. '.';
	end
 
	if ( src.issue ) then
		result = result .. ' — № ' .. toString(src.issue, options_commas ) .. '.';
	end
 
	if ( src.page ) then
		local letter = i18nPage[ src.lang ] or i18nPage[ i18nDefaultLanguage ];
		result = result .. ' — ' .. letter .. ' ' .. toString(src.page, options_commas )  .. '.';
	end
 
	if ( src.isbn13 ) then
		result = result .. ' — ISBN ' .. toString( src.isbn13, options_commas );
	elseif ( src.isbn10 ) then
		result = result .. ' — ISBN ' .. toString( src.isbn10, options_commas );
	end

	if ( src.issn ) then
		result = result .. ' — ISSN ' .. toString( src.issn, options_commas );
	end
	if ( src.doi ) then
		result = result .. ' — [http://dx.doi.org/' .. mw.uri.encode( src.doi ) .. ' DOI ' .. src.doi .. ']';
	end

	if ( src.entityId ) then
		if ( src.type and src.entityId ) then
			-- wrap into span to target from JS
			result = '<span class="' .. toString( src.type, options_citetypes ) .. '" data-entity-id="' .. getSingle( src.entityId ) .. '">' .. result .. '</span>'
		else
			result = '<span class="citetype_unknown" data-entity-id="' .. getSingle( src.entityId ) .. '">' .. result .. '</span>'
		end
	end

	return {text = result, code = src.code};
end

function renderShortReference( src )
	src.lang = getSingle( src.lang ) or i18nDefaultLanguage;
	src.title = src.title or '\'\'(unspecified title)\'\''

	local result = '[[#' .. PREFIX_CITEREF .. src.code .. '|';
	if ( src.authors ) then
		result = result .. toString( src.authors, options_commas_it_nolinks );
	else
		result = result .. toString( src.title, options_commas_it_nolinks );
	end
	result = result .. ']]'

	if ( src.year ) then
		result = result .. ', ' .. toString( src.year, options_commas );
	end

	if ( src.volume ) then
		local letter = i18nVolume[ src.lang ] or i18nVolume[ i18nDefaultLanguage ];
		result = result .. ' — ' .. letter .. '&nbsp;' .. toString(src.volume, options_commas ) .. '.';
	end
 
	if ( src.issue ) then
		result = result .. ' — №&nbsp;' .. toString(src.issue, options_commas ) .. '.';
	end
 
	if ( src.page ) then
		local letter = i18nPage[ src.lang ] or i18nPage[ i18nDefaultLanguage ];
		result = result .. ' — ' .. letter .. '&nbsp;' .. toString(src.page, options_commas )  .. '.';
	end
end

function getSingle( value )
	if ( not value ) then
		return;
	end
	if ( type( value ) == 'string' ) then
		return value;
	elseif ( type( value ) == 'table' ) then
		if ( value.id ) then
			return value.id;
		end

		for i, tableValue in pairs( value ) do
			return getSingle( tableValue );
		end
	end

	return '(unknown)';
end

function toString( value, options )
	if ( type( value ) == 'string' ) then
		return options.format( value );
	elseif ( type( value ) == 'table' ) then
		if ( value.id ) then
			-- this is link
			if ( options.preferids ) then
				return options.format( value.id );
			else
				if ( options.nolinks ) then
					return options.format( value.label or mw.wikibase.label( value.id ) or '\'\'(untranslated title)\'\'' );
				else
					return options.format( renderLink( value.id, value.label ) );
				end
			end
		end

		local resultList = {};
		for i, tableValue in pairs( value ) do
			table.insert( resultList, toString( tableValue, options ) );
		end

		return mw.text.listToText( resultList, options.separator, options.conjunction);
	else
		return options.format( '(unknown type)' );
	end

	return '';
end

function renderLink( entityId, text )
	if ( not entityId ) then
		error("entityId is not specified");
	end
	local actualText = text or mw.wikibase.label( entityId ) or '\'\'(untranslated)\'\'';
	local link = mw.wikibase.sitelink( entityId ) or ( ':d:' .. entityId )
	return '[[' .. link .. '|' .. actualText .. ']]';
end

-- Expand special types of references when additional data could be found in OTHER entity properties
function expandSpecials( currentEntity, reference, data )
	if ( reference.snaks.p248
			and reference.snaks.p248[0]
			and reference.snaks.p248[0].datavalue
			and reference.snaks.p248[0].datavalue.value["numeric-id"]) then
		local sourceId = "Q" .. reference.snaks.p248[0].datavalue.value["numeric-id"];

		-- Gemeinsame Normdatei -- specified by P227
		if ( sourceId == 'Q36578' ) then
			appendMainSnaks( currentEntity, 'p227', data, 'title', { format = function( gnd ) return 'Record #' .. gnd; end } );
			appendMainSnaks( currentEntity, 'p227', data, 'url', { format = function( gnd ) return 'http://d-nb.info/gnd/' .. gnd .. '/'; end } );
		end

		-- Gran Enciclopèdia Catalana -- specified by P1296
		if ( sourceId == 'Q2664168' ) then
			appendMainSnaks( currentEntity, 'P1296', data, 'url', { format = function( id ) return 'http://www.enciclopedia.cat/enciclop%C3%A8dies/gran-enciclop%C3%A8dia-catalana/EC-GEC-' .. id .. '.xml'; end } );
			expandSpecialsQualifiers( currentEntity, 'P1296', data );
		end

		-- Encyclopædia Britannica online -- specified by P1417
		if ( sourceId == 'Q5375741' ) then
			appendMainSnaks( currentEntity, 'P1417', data, 'url', { format = function( id ) return 'http://global.britannica.com/' .. id; end } );
			expandSpecialsQualifiers( currentEntity, 'P1417', data );
		end

		-- Electronic Jewish Encyclopedia (Elektronnaja Evrejskaja Entsiklopedia) -- specified by P1438
		if ( sourceId == 'Q1967250' ) then
			appendMainSnaks( currentEntity, 'P1438', data, 'url', { format = function( id ) return 'http://www.eleven.co.il/article/' .. id; end } );
			expandSpecialsQualifiers( currentEntity, 'P1438', data );
		end

		-- sports-reference.com -- specified by P1447
		if ( sourceId == 'Q18002875' ) then
			appendMainSnaks( currentEntity, 'P1447', data, 'url', { format = function( id ) return 'http://www.sports-reference.com/olympics/athletes/' .. id .. '.html'; end } );
			expandSpecialsQualifiers( currentEntity, 'P1447', data );
		end

		-- do we have appropriate record in P1343 ?
		local claims = findClaimsByValue( currentEntity, 'p1343', sourceId );
		if ( claims and #claims ~= 0 ) then
			appendQualifiers( claims, 'P958', data, 'part', {} );
			appendQualifiers( claims, 'P854', data, 'url', {} );
			appendQualifiers( claims, 'P357', data, 'title', {} );
			appendQualifiers( claims, 'P478', data, 'volume', {} );
		end
	end
end

function expandSpecialsQualifiers( entity, propertyId, result )
	if ( entity.claims and entity.claims[propertyId] ) then
		local claims = entity.claims[propertyId];
		appendQualifiers( claims, 'P958', result, 'part', {} );
		appendQualifiers( claims, 'P854', result, 'url', {} );
		appendQualifiers( claims, 'P357', result, 'title', {} );
		appendQualifiers( claims, 'P478', result, 'volume', {} );
	end
end

function findClaimsByValue( entity, propertyId, value )
	local result = {};
	if ( entity.claims and entity.claims[propertyId] ) then
		for i, claim in pairs( entity.claims[propertyId] ) do
			if ( claim.mainsnak and claim.mainsnak.datavalue ) then
				local datavalue = claim.mainsnak.datavalue;
				if ( datavalue.type == "string" and datavalue.value == value 
					or datavalue.type == "wikibase-entityid" and datavalue.value["entity-type"] == "item" and tostring( datavalue.value["numeric-id"] ) == mw.ustring.sub( value, 2 ) ) then
					table.insert( result, claim );
				end
			end
		end
	end
	return result;
end

function appendMainSnaks( entity, propertyId, result, property, options )
	if ( entity.claims and entity.claims[propertyId] ) then
		for i, claim in pairs( entity.claims[propertyId] ) do
			if ( claim.mainsnak and claim.mainsnak.datavalue ) then
				appendImpl( claim.mainsnak.datavalue, result, property, options );
			end
		end
	end
end

function appendSnaks( allSnaks, snakPropertyId, result, property, options )
	if ( allSnaks and allSnaks[ snakPropertyId ] ) then
		for k, snak in pairs( allSnaks[ snakPropertyId ] ) do
			if ( snak and snak.datavalue ) then
				appendImpl( snak.datavalue, result, property, options );
			end
		end
	end
end

function appendQualifiers( claims, qualifierPropertyId, result, property, options )
	for i, claim in pairs( claims ) do
		if ( claim.qualifiers and claim.qualifiers[ qualifierPropertyId ] ) then
			for k, qualifier in pairs( claim.qualifiers[ qualifierPropertyId ] ) do
				if ( qualifier and qualifier.datavalue ) then
					appendImpl( qualifier.datavalue, result, property, options );
				end
			end
		end
	end
end

function appendImpl( datavalue, result, property, options )
	if ( datavalue.type == 'string' ) then
		local value = datavalue.value;
		if ( options.format ) then
			value = options.format( value );
		end
		if ( not result[property] ) then
			result[property] = {};
		elseif ( type( result[property] ) == 'string' or ( type( result[property] ) == 'table' and type( result[property].id ) == 'string' ) ) then
			result[property] = { result[property] };
		end
		table.insert( result[property], value);
	end
	if ( datavalue.type == 'wikibase-entityid' ) then
		local value = datavalue.value;
		if ( not result[property] ) then
			result[property] = {};
		elseif ( type( result[property] ) == 'string' or ( type( result[property] ) == 'table' and type( result[property].id ) == 'string' ) ) then
			result[property] = { result[property] };
		end
		table.insert( result[property], { id = 'Q' .. value["numeric-id"] });
	end
end

function expandPublication( data )
	local publication = data.publication;

	-- use only first one
	if ( type( publication ) == 'table' and publication[1] and publication[1].id ) then
		data.publication = publication[1];
		publication = data.publication;
	end

	if ( publication and publication.id ) then
		populateSourceData( data, publication.id );
	end
end

function loadSafe( entityId )
	if ( entityId == nil ) then
		error('entityId to load is not specified');
	end
	local status, result = pcall( function() return mw.loadData( 'Module:Source/' .. entityId ) end );
	if ( status == true ) then
		return true, result;
	end
	return false, nil;
end

function populateSourceData( data, sourceId )
	local loaded, sourceData = loadSafe( sourceId );
	if ( loaded and sourceData ) then
		populateSourceDataImpl( data, sourceData );
	end
end

function populateSourceDataImpl( data, sourceData )
	for key, value in pairs( sourceData ) do
		if ( not data[key] and key ~= 'title' ) then
			data[key] = value;
		end
	end

	-- if we already have title, than it would be the current one, otherwise move it to publication
	if ( sourceData.title ) then
		if ( not data.title ) then
			data.title = sourceData.title;
		else
			if ( not data.publication ) then
				data.publication = sourceData.title;
			end
		end
	end
end

function updateWithRef( reference, src )
	-- specified
	if ( reference.snaks.p662 ) then
		local cid = reference.snaks.p662[0].datavalue.value;
		src.code = src.code .. '-cid:' .. cid;
		src.title = 'Compound Summary for: CID ' .. cid;
		src.url = 'http://pubchem.ncbi.nlm.nih.gov/summary/summary.cgi?cid=' .. cid;
		src.publication = { id = 'Q278487', label = 'PubChem' };
	end

	appendSnaks( reference.snaks, 'P364', src, 'lang', {} );
	appendSnaks( reference.snaks, 'P958', src, 'part', {} ); -- part
	appendSnaks( reference.snaks, 'P357', src, 'title', {} ); -- title
	appendSnaks( reference.snaks, 'P854', src, 'url', {} );
	appendSnaks( reference.snaks, 'P123', src, 'publisher', {} );
	appendSnaks( reference.snaks, 'P304', src, 'page', {} );
	appendSnaks( reference.snaks, 'P478', src, 'volume', {} );
	return src;
end

function p.renderSource( frame )
	local arg = frame.args[1];
	return p.renderSourceImpl( arg );
end

function p.renderSourceImpl( entityId )
	local value = {};
	value["numeric-id"] = string.sub( entityId , 2);
	local snak = { datavalue = { value =value } };
	local properties = {};
	properties[0] = snak;
	return renderReferenceImpl( {}, { snaks = { p248 = properties } } ).text;
end

function p.renderReference( frame, currentEntity, reference )
	
	-- template call
	if ( frame and not currentEntity and not reference ) then
		local value = {};
		value["numeric-id"] = string.sub( frame.args[1] , 2);
		local snak = { datavalue = { value =value } };
		local properties = {};
		properties[0] = snak;
		
		currentEntity = mw.wikibase.getEntity();
		reference = { snaks = { p248 = properties } };
	end

	local rendered = renderReferenceImpl( currentEntity, reference );

	if ( not rendered ) then
		return '';
	end

	local result;
	local code = rendered.code or mw.text.encode( rendered.text );
	result = frame:extensionTag( 'ref', rendered.text, {name = code} ) .. '';

	if ( not rendered.found ) then
		result = result .. '';
	end

	return result;
end

function renderReferenceImpl( currentEntity, reference )
	if ( not reference.snaks ) then
		return nil;
	end

	local data = {};
	local entityId, found, sourceData;
	if ( reference.snaks.p248 ) then
		entityId = "Q" .. reference.snaks.p248[0].datavalue.value["numeric-id"];
		found, sourceData = loadSafe( entityId );
		data.code = entityId;
		data.entityId = entityId;
	else
		found = true;
	end

	updateWithRef( reference, data );
	expandSpecials( currentEntity, reference, data );
	if ( entityId ) then
		if ( found and sourceData ) then
			populateSourceDataImpl( data, sourceData );
		else
			if ( data.title ) then
				data.publication = data.publication or { id = entityId, label = mw.wikibase.label( entityId ) };
			else
				data.title = { id = entityId, label = mw.wikibase.label( entityId ) };
			end
		end
	end
	expandPublication( data );

	local rendered;
	if ( p.short ) then
		local toStore = p.deepcopy( data );
		if (not p.list ) then
			p.list = {};
		end
		p.list[toStore.code] = toStore;
		rendered = renderShortReference( data );
	else
		rendered = renderSource( data );
	end

	if ( mw.ustring.len( rendered.text ) == 0 ) then
		return nil;
	end

	rendered.found = found;
	return rendered;
end

return p;