--script that retrieves basic data stored in Wikidata, for the datamodel, see https://www.mediawiki.org/wiki/Extension:Wikibase_Client/Lua

local wd = {}

local modules = { }
local modulesNames = {
	reference = 'Module:Wikidata/Références',
	i18n = 'Module:Wikidata/I18n',
	globes = 'Module:Wikidata/Globes',
	langcodes = 'Module:Dictionnaire Wikidata/Codes langue', -- big, infrequently useda
	invertedlangcodes = 'Module:Dictionnaire Wikidata/Codes langue/inversé',

	linguistic = 'Module:Linguistique',
	formatDate = 'Module:Date complexe2',
	formatNum = 'Module:Conversion',
	langmodule = 'Module:Langue',
	cite = 'Module:Biblio',
	weblink = 'Module:Weblink'
}

local function loadModule( t, key )
	if modulesNames[key] then
		local m = require( modulesNames[key] )
		t[key] = m
		return m
	end
end
setmetatable( modules, { __index = loadModule } )

local datequalifiers = {'P585', 'P571', 'P580', 'P582', 'P1319', 'P1326'}

-- === I18n ===
local defaultlang = mw.getContentLanguage():getCode()

function wd.translate(str, rep1, rep2)
	str = modules.i18n[str] or str
	if rep1 and (type (rep1) == 'string') then
		str = str:gsub('$1', rep1)
	end
	if rep2 and (type (rep2) == 'string')then
		str = str:gsub('$2', rep2)
	end
	return str
end

local function addCat(cat, sortkey)
	if sortkey then
		return  '[[Category:' .. cat .. '|' .. sortkey .. ']]'
	end
	return '[[Category:' .. cat  .. ']]'
end

local function formatError( key , category, debug)
    if debug then
        return error(modules.i18n[key] or key)
    end
    if category then
        return addCat(category, key)
    else
        return addCat('cat-unsorted-issue', key)
    end
end

-- 

function wd.isSpecial(snak)
	return (snak.snaktype ~= 'value')
end

function wd.getId(snak)
	if (snak.snaktype == 'value') then
		return 'Q' .. snak.datavalue.value['numeric-id']
	end
end

function wd.getNumericId(snak)
	if (snak.snaktype == 'value') then
		return snak.datavalue.value['numeric-id']
	end
end

function wd.getMainId(claim)
	return wd.getId(claim.mainsnak)
end

function wd.entityId(entity)
	if type(entity) == 'string' then
		return entity
	elseif type(entity) == 'table' then
		return entity.id
	end
end

function wd.getEntityIdForCurrentPage()
	return mw.wikibase.getEntityIdForCurrentPage()
end

-- function that returns true if the "qid" parameter is the qid 
-- of the item that is linked to the calling page
function wd.isPageOfQId(qid) 
	local self_id = mw.wikibase.getEntityIdForCurrentPage() 
	return self_id ~= nil and qid == self_id 
end

function wd.getEntity( val ) 
	if type(val) == 'table' then
		return val
	end
	if val == '-' then
		return nil
	end
	if val == '' then
		val = nil
	end
	return mw.wikibase.getEntity(val)
end

function wd.splitStr(val) -- transforme en table les chaînes venant du Wikitexte qui utilisent des virgules de séparation
	if type(val) == 'string' then
		val = mw.text.split(val, ",")
	end
	return val
end

function wd.isHere(searchset, val)
	for i, j in pairs(searchset) do
		if val == j then
			return true
		end
	end
	return false
end


local function wikidataLink(entity)
	local name =':d:'
	
	if type(entity) == 'string' then
		if entity:match("P[0-9+]") then
			entity = "Property:" .. entity
		end
		return name .. entity
	elseif type(entity) == 'table' then
		if entity["type"] == "property" then
			name = ":d:Property:"
		end
		return name .. entity.id
	elseif type(entity) == nil then
		return formatError('entity-not-found')
	end
end

function wd.siteLink(entity, project, lang)
	-- returns 3 values: a sitelink (with the relevant prefix) a project name and a language
	lang = lang or defaultlang
	if (type(project) ~= 'string') then
		project = 'wiki'
	end
	project = project:lower()
	if project == 'wikipedia' then
		project = 'wiki'
	end
	if type(entity) == 'string' and (project == 'wiki') and ( (not lang or lang == defaultlang) ) then -- évite de charger l'élément entier
		return  mw.wikibase.sitelink(entity), 'wiki', defaultlang
	end
	if project == 'wikidata' then
		return wikidataLink(entity), 'wikidata'
	end
	local projects = {
		-- nom = {préfixe sur Wikidata, préfix pour les liens sur Wikipédia, ajouter préfixe de langue}
		wiki = {'wiki', nil, true}, -- wikipedia
		commons = {'commonswiki', 'commons', false},
		commonswiki = {'commonswiki', 'commons', false},
		wikiquote = {'wikiquote', 'q', true},
		wikivoyage = {'wikivoyage', 'voy', true},
		wikibooks = {'wikibooks', 'b', true},
		wikinews = {'wikinews', 'n', true},
		wikiversity = {'wikiversity', 'v', true},
		wikisource = {'wikisource', 's', true},
		wiktionary = {'wiktionary', 'wikt', true},
		specieswiki = {'specieswiki', 'species', false},
		metawiki = {'metawiki', 'm', false},
		incubator = {'incubator', 'incubator', false},
		outreach = {'outreach', 'outreach', false},
		mediawiki = {'mediawiki', 'mw', false}
	}

	local entityid = entity.id or entity

	local projectdata = projects[project:lower()]
	if not projectdata then -- defaultlink might be in the form "dewiki" rather than "project: 'wiki', lang: 'de' "
		for k, v in pairs(projects) do
			if project:match( k .. '$' ) 
				and mw.language.isKnownLanguageTag(project:sub(1, #project-#k))
			then
				lang = project:sub(1, #project-#k)
				project = project:sub(#lang + 1, #project)
				projectdata = projects[project]
				break
			end
		end
		if not mw.language.isKnownLanguageTag(lang) then
			return --formatError('invalid-project-code', projet or 'nil')
		end
	end
	if not projectdata then
		return -- formatError('invalid-project-code', projet or 'nil')
	end
	
	local linkcode = projectdata[1]
	local prefix = projectdata[2]
	local multiversion = projectdata[3]
	if multiversion then
		linkcode = lang .. linkcode
	end
	local link = mw.wikibase.getSitelink(entityid, linkcode)
	if not link then
		return nil
	end
	
	if prefix then
		link = prefix .. ':' .. link
	end
	if multiversion then
		link = ':' .. lang .. ':' .. link
	end
	return link, project, lang
end

-- add new values to a list, avoiding duplicates
function wd.addNewValues(olditems, newitems, maxnum, stopval)
	if not newitems then
		return olditems
	end
	for _, qid in pairs(newitems) do
		if stopval and (qid == stopval) then
			table.insert(olditems, qid)
			return olditems
		end
		if maxnum and (#olditems >= maxnum) then
			return olditems
		end
		if not wd.isHere(olditems, qid) then
			table.insert(olditems, qid)
		end
	end
	return olditems
end

--=== FILTER CLAIMS ACCORDING TO VARIOUS CRITERIA : FUNCTION GETCLAIMS et alii ===

local function notSpecial(claim)
	local type
	
	if claim.mainsnak ~= nil then
		type = claim.mainsnak.snaktype
	else
		-- condition respectée quand showonlyqualifier est un paramètre renseigné
		-- dans ce cas, claim n'est pas une déclaration entière, mais UNE snak qualifiée du main snak
		type = claim.snaktype
	end
	
	return type == 'value'
end

local function hasTargetValue(claim, targets) -- retourne true si la valeur est dans la liste des target, ou si c'est une valeur spéciale filtrée séparément par excludespecial
	local id = wd.getMainId(claim)
	local targets = wd.splitStr(targets)
	return wd.isHere(targets, id) or wd.isSpecial(claim.mainsnak)
end

local function excludeValues(claim, values) -- true si la valeur n'est pas dans la liste, ou si c'est une valeur spéciale (filtrée à part par excludespecial)
	return wd.isSpecial(claim.mainsnak) or not ( hasTargetValue(claim, values) )
end

local function bestRanked(claims)
	if not claims then
		return nil
	end
	local preferred, normal = {}, {}
	for i, j in pairs(claims) do
		if j.rank == 'preferred' then
			table.insert(preferred, j)
		elseif j.rank == 'normal' then
			table.insert(normal, j)
		end
	end
	if #preferred > 0 then
		return preferred
	else
		return normal
	end
end

local function withRank(claims, target)
	if target == 'best' then
		return bestRanked(claims)
	end
	local newclaims = {}
	for pos, claim in pairs(claims)  do
		if target == 'valid' then
			if claim.rank ~= 'deprecated' then
				table.insert(newclaims, claim)
			end
		elseif claim.rank == target then
			table.insert(newclaims, claim)
		end
	end
	return newclaims
end

function wd.hasQualifier(claim, acceptedqualifs, acceptedvals, excludequalifiervalues)
	local claimqualifs = claim.qualifiers
	
	if (not claimqualifs) then
		return false
	end

	acceptedqualifs = wd.splitStr(acceptedqualifs)
	acceptedvals = wd.splitStr( acceptedvals)


	local function ok(qualif) -- vérification pour un qualificatif individuel
		if not claimqualifs[qualif] then
			return false
		end
		if not (acceptedvals) then  -- si aucune valeur spécifique n'est demandée, OK
			return true
		end
		for i, wanted in pairs(acceptedvals) do
			for j, actual in pairs(claimqualifs[qualif]) do
				if wd.getId(actual) == wanted then
					return true
				end
			end
		end
	end

	for i, qualif in pairs(acceptedqualifs) do
		if ok(qualif) then
			return true
		end
	end
	return false
end

local function hasSource(claim, targetsource, sourceproperty)
	sourceproperty = sourceproperty or 'P248'
	if targetsource == "-" then
		return true
	end
	if (not claim.references) then return
		false
	end
	local candidates = claim.references[1].snaks[sourceproperty] -- les snaks utilisant la propriété demandée
	if (not candidates) then
		return false
	end
	if (targetsource == "any") then -- si n'importe quelle valeur est acceptée tant qu'elle utilise en ref la propriété demandée
		return true
	end
	targetsource = wd.splitStr(targetsource)
	for _, source in pairs(candidates) do
		local s = wd.getId(source)
		for i, target in pairs(targetsource) do
			if s == target then return true end
		end
	end
	return false
end

local function excludeQualifier(claim, qualifier, qualifiervalues)
	return not wd.hasQualifier(claim, qualifier, qualifiervalues)
end

local function hasDate(claim)
	local claimqualifs = claims.qualifiers
	if not claimqualifs then
		return false
	end
	for _, qualif in pairs(claimqualifs) do
		if claimsqualifs[qualif] and claimsqualifs[qualif][1].snaktype == 'value' then
			return true
		end
	end
	return false
end

local function hasLink(claim, site, lang)
	if (claim.mainsnak.snaktype ~= 'value') then -- ne pas supprimer les valeurs spéciales, il y a une fonction dédiée pour ça
		return true
	end
	local id = wd.getMainId(claim)
	local link = wd.siteLink(id, site, lang)
	if link then
		return true
	end
end

local function isInLanguage(claim, lang) -- ne fonctionne que pour les monolingualtext / étendre aux autres types en utilisant les qualifiers ?
	if type(lang) == 'table' then -- si c'est une table de language séparées par des virgules, on les accepte toutes
		for i, l in pairs(lang) do
			local v = isInLanguage(claim, l)
			if v then
				return true
			end
		end
	end
	if type(lang) ~= ('string') then
		return --?
	end

	if (lang == '-') then
		return true
	end
	if (lang == 'locallang') then
		lang =  mw.getContentLanguage():getCode()
	end
	
	-- pour les monolingual text
	local snak = claim.mainsnak or claim
	if snak.snaktype == 'value' and snak.datavalue.type == 'monolingualtext' then
		if snak.datavalue.value.language == lang then
			return true
		end
		return false
	end

	-- pour les autres types de données : recherche dans les qualificatifs
	if (lang == 'fr') then
		lang = 'Q150'
	elseif (lang == 'en') then
		lang = 'Q1860'
	else
		lang = invertedlangcodes[lang]
	end
	if claim.qualifiers and claim.qualifiers.P407 then
		if wd.hasQualifier(claim, {'P407'}, {lang}) then
			return true
		else
			return false
		end
	end

	return true -- si on ne ne sait pas la langue, on condière que c'est bon
end

local function firstVals(claims, numval) -- retourn les numval premières valeurs de la table claims
    local numval = tonumber(numval) or 0 -- raise a error if numval is not a positive integer ?
    if not claims then
    	return nil
    end
    while (#claims > numval) do
    	table.remove(claims)
    end
    return claims
end

local function lastVals(claims, numval2) -- retourn les valeurs de la table claims à partir de numval2
    local numval2 = tonumber(numval2) or 0 -- raise a error if numval is not a positive integer ?
    if not claims then
    	return nil
    end
    for i=1,numval2 do
    	table.remove(claims, 1)
    end
    return claims
end

local function timeFromQualifs(claim, qualifs)
	local claimqualifs = claim.qualifiers
	if not claimqualifs then
		return nil
	end
	for i, qualif in pairs(qualifs or timequalifiers) do
		local vals = claimqualifs[qualif]
		if vals and (vals[1].snaktype == 'value') then
			return vals[1].datavalue.value.time
		end
	end
end

local function atDate(claim, mydate)
	if mydate == "today" then
		mydate = os.date("!%Y-%m-%dT%TZ")
	end	
	local newclaims = {}
	local mindate = timeFromQualifs(claim, {'P580'}) 
	local maxdate = timeFromQualifs(claim, {'P582'})
	if modules.formatDate.before(mydate, mindate) and  modules.formatDate.before(maxdate, mydate) then
		return true
	end
end

local function check(claim, condition)
	if type(condition) == 'function' then -- cas standard
		return condition(claim)
	end
	return formatError('invalid type', 'function', type(condition)) 
end

local function minPrecision(claim, minprecision)
	local snack
	if claim.qualifiers then -- si une date est donnée en qualificatif, c'est elle qu'on utilise de préférence au mainsnak
		for i, j in ipairs(datequalifiers) do
			if claim.qualifiers[j] then
				snack = claim.qualifiers[j][1] 
				break
			end
		end
	end
	if not snack then
		snack = claim.mainsnak or claim
	end
	if (snack.snaktype == 'value') and (snack.datatype == 'time') and (snack.datavalue.value.precision < minprecision) then
		return false
	end
	return true
end

function wd.sortClaims(claims, sorttype)
	if not claims then
		return nil
	end
	if sorttype == 'chronological' then
		return wd.chronoSort(claims)
	elseif sorttype == 'inverted' then
		return wd.chronoSort(claims, true)
	elseif sorttype == 'order' then
		return wd.chronoSort(claims)
	elseif type(sorttype) == 'function' then
		table.sort(claims, sorttype)
		return claims
	end
	return claims
end
local alreadyFiltered = false
function wd.filterClaims(claims, args) --retire de la tables de claims celles qui sont éliminés par un des filters de la table des filters

	local function filter(condition, filterfunction, funargs)
		if not args[condition] then
			return
		end
		for i = #claims, 1, -1 do
			if not( filterfunction(claims[i], args[funargs[1]], args[funargs[2]], args[funargs[3]]) ) then
				table.remove(claims, i)
			end
		end
	end
	filter('isinlang', isInLanguage, {'isinlang'} )
	filter('excludespecial', notSpecial, {} )
	filter('condition', check, {'condition'} )
	if claims[1] and claims[1].mainsnak then
		filter('targetvalue', hasTargetValue, {'targetvalue'} )
		filter('atdate', atDate, {'atdate'} )
		filter('qualifier', wd.hasQualifier, {'qualifier', 'qualifiervalue'} )
		filter('excludequalifier', excludeQualifier, {'excludequalifier', 'excludequalifiervalue'} )
		filter('withsource', hasSource, {'withsource', 'sourceproperty'} )
		filter('withdate', hasDate, {} )
		filter('excludevalues', excludeValues, {'excludevalues'})
		filter('withlink', hasLink, {'withlink', 'linklang'} )
		filter('minprecision', minPrecision, {'minprecision'} )
		claims = withRank(claims, args.rank or 'best')
	end
	if args.sorttype then
		claims = wd.sortClaims(claims, args.sorttype)
	end
	if #claims == 0 then
		return nil
	end
	if args.numval2 then
		claims = lastVals(claims, args.numval2)
	end
	if args.numval then
		claims = firstVals(claims, args.numval)
	end
	return claims

end

function wd.loadEntity(entity, cache)
	if type(entity) ~= 'table' then
		if cache then
			if not cache[entity] then 
				cache[entity] = mw.wikibase.getEntity(entity)
				mw.log("cached")
     		end
			return cache[entity]
        else
			if entity == '' or (entity == '-') then
				entity = nil
			end
        	return mw.wikibase.getEntity(entity)
        end
    else 
    	return entity
    end
end


function wd.getClaims( args ) -- returns a table of the claims matching some conditions given in args
	if args.claims then -- if claims have already been set, return them
		return args.claims
	end
	local properties = args.property
	if type(properties) == 'string' then
		properties = wd.splitStr(string.upper(args.property))
	end
	if not properties then
		return formatError( 'property-param-not-provided' )
	end

	--Get entity
	local entity = args.entity
	if type(entity) == 'string' then
		if entity == '' then
			entity = nil
		end
	elseif type(entity) == 'table' then
		entity = entity.id
	end
        if (not entity) then
            entity = mw.wikibase.getEntityIdForCurrentPage()
        end
	if (not entity) or (entity == '-') then
		return nil
	end
	local claims = {}

	if #properties == 1 then
		claims = mw.wikibase.getAllStatements(entity, properties[1]) -- do not use mw.wikibase.getBestStatements at this stage, as it may remove the best ranked values that match other criteria in the query
	else
		for i, prop in ipairs(properties) do
			local newclaims = mw.wikibase.getAllStatements(entity, prop) 
			if newclaims and #newclaims > 0 then
				for j, claim in ipairs(newclaims) do
					table.insert(claims, claim)
				end
			end
		end
	end


	if (not claims) or (#claims == 0) then
		return nil
	end
	return wd.filterClaims(claims, args)
end

--=== ENTITY FORMATTING ===

function wd.getLabel(entity, lang, labelformat)
	
	if (not entity) then
		return nil -- ou option de gestion des erreurs ?
	end

	lang = lang or defaultlang

	if type(labelformat) == 'function' then
		return labelformat(entity)
	end
	
	entity = entity.id or ( type(entity) == "string" and entity)
	
	if (type(entity) == 'string') and (lang == defaultlang) then -- le plus économique
		local str = mw.wikibase.label(entity)
		if str then -- mw.wikibase.label() ne fonctionne pas avec les redirect https://phabricator.wikimedia.org/T157868
			return str
		end
	end
	
	return mw.wikibase.getLabelByLang(entity, lang)
end

function wd.formatEntity( entity, params )

	if (not entity) then
		return nil --formatError('entity-not-found')
	end
	local id = entity
	if type(id) == 'table' then
		id = id.id
	end

	params = params or {}
	local lang = params.lang or defaultlang
	local speciallabels = params.speciallabels
	local displayformat = params.displayformat
	local labelformat = params.labelformat
	local defaultlabel = params.defaultlabel or id
	local linktype = params.link
	local defaultlink = params.defaultlink
	local defaultlinkquery = params.defaultlinkquery

	if speciallabels and speciallabels[id] then --speciallabels override the standard label + link combination
		return speciallabels[id]
	end
	if params.displayformat == 'raw' then
		return id
	end

	local link, label
	
	label = wd.getLabel(entity, lang, labelformat)
	
	-- détermination du fait qu'on soit ou non en train de rendre l'élément sur la page de son article
	local rendering_entity_on_its_page = wd.isPageOfQId(id)
		

	if not label then
		if (defaultlabel == '-') then 
			return nil
		end
		link = wd.siteLink(id, 'wikidata')
		return '[[' .. link .. '|' .. id .. ']]' .. addCat(modules.i18n['to translate'])
-- si pas de libellé, on met un lien vers Wikidata pour qu'on comprenne à quoi ça fait référence
	end

	if (linktype == '-') or rendering_entity_on_its_page then
		return label
	end

	local link = wd.siteLink(entity, linktype, lang)

	-- defaultlinkquery will try to link to another page on this Wiki
	if (not link) and defaultlinkquery then
		if type(defaultlinkquery) == 'string' then
				defaultlinkquery = {property = defaultlinkquery}
		end
		defaultlinkquery.excludespecial = true
		defaultlinkquery.entity = entity
		local claims = wd.getClaims(defaultlinkquery)
		if claims then
			for i, j in pairs(claims) do
				local id = wd.getMainId(j)
				link = wd.siteLink(id, linktype, lang)
				if link then
					break
				end
			end
		end	
	end

	if link then 
		return '[[' .. link .. '|' .. label .. ']]'
	end

	-- if not link, you can use defaultlink: a sidelink to another Wikimedia project
	if (not defaultlink) then
		defaultlink = {'enwiki'}
	end
	if defaultlink and (defaultlink ~= '-') then
		local linktype
		local sidelink, site, langcode
	
		if type(defaultlink) == 'string' then
			defaultlink = {defaultlink}
		end
		for i, j in ipairs(defaultlink) do
			sidelink, site, langcode = wd.siteLink(entity, j, lang)
			if sidelink then
				break
			end
		end
		if not sidelink then
			sidelink, site = wd.siteLink(entity, 'wikidata')
		end
		
		local icon, class, title = site, nil, nil -- le texte affiché du lien
		if site == 'wiki' then
			icon, class, title = langcode, "indicateur-langue", wd.translate('see-another-language', mw.language.fetchLanguageName(langcode, defaultlang))	
		elseif site == 'wikidata' then
			icon, class, title = 'd',  "indicateur-langue", wd.translate('see-wikidata')		
		else
			title = wd.translate('see-another-project', site)
		end
		local val = '[[' .. sidelink .. '|' .. '<span class = "' .. (class or '').. '" title = "' .. (title or '') .. '">' .. icon .. '</span>]]'
		return label .. ' <small>(' .. val .. ')</small>'
	end 
	return label
end


function wd.addTrackingCat(prop, cat) -- doit parfois être appelé par d'autres modules
	if type(prop) == 'table' then
		prop = prop[1] -- devrait logiquement toutes les ajouter
	end
	if not prop and not cat then
		return formatError("property-param-not-provided")
	end
	if not cat then
		cat = wd.translate('trackingcat', prop or 'P??')
	end
	return addCat(cat )
end

local function unknownValue(snak, label)
	local str = label

	if type(str) == "function" then
		str = str(snak)
	end

	if (not str) then
		if snak.datatype == 'time' then
			str = wd.translate('sometime')
		else
			str = wd.translate('somevalue')
		end
	end

	if type(str) ~= "string" then
		return formatError(snak.datatype)
	end
	return str
end

local function noValue(displayformat)
	if not displayformat then
		return wd.translate('novalue')
	end
	if type(displayformat) == 'string' then
		return displayformat
	end
	return formatError()
end

local function getLangCode(entityid)
	return modules.langcodes[tonumber(entityid:sub(2))]
end

local function showLang(statement) -- retourne le code langue entre paranthèse avant la valeur (par exemple pour les biblios et liens externes)
	local mainsnak = statement.mainsnak
	if mainsnak.snaktype ~= 'value' then
		return nil
	end
	local langlist = {}
	if mainsnak.datavalue.type == 'monolingualtext' then
		langlist = {mainsnak.datavalue.value.language}
	elseif (not statement.qualifiers) or (not statement.qualifiers.P407) then
		return
	else
		for i, j in pairs( statement.qualifiers.P407 ) do
			if  j.snaktype == 'value' then
				local langentity = wd.getId(j)
				local langcode =  getLangCode(langentity)
				table.insert(langlist, langcode)
			end
		end
	end
	if (#langlist > 1) or (#langlist == 1 and langlist[1] ~= defaultlang) then -- si c'est en français, pas besoin de le dire
		return modules.langmodule.indicationMultilingue(langlist)
	end
end


-- === DATE HANDLING ===

function wd.addStandardQualifs(str, statement)
	if (not statement) or (not statement.qualifiers) then
		return str
	end
	if not str then
		return error()-- what's that ?
	end

	if statement.qualifiers.P1480 then
		for i, j in pairs(statement.qualifiers.P1480) do
			local v = wd.getId(j)
			if (v == "Q21818619") then
				str = wd.translate('approximate-place', str)
			elseif (v == "Q18122778") or (v == "Q18912752") then
				str = wd.translate('uncertain-information', str)
			elseif (v == "Q5727902") then
				if (statement.mainsnak.datatype == 'time') then
					str = modules.formatDate.fuzzydate(str)
				else
					str = wd.translate('approximate-value', str)
				end
			end			
		end
	end
	return str
end

local function rangeObject(begin, ending, params)
	--[[
		objet comportant un timestamp pour le classement chronologique et deux dateobject (begin et ending)
	]]-- 
	local timestamp
	if begin then
		timestamp = begin.timestamp
	else
		timestamp = ending.timestamp
	end
	return {begin = begin, ending = ending, timestamp = timestamp, type = 'rangeobject'}
end

local function dateObject(orig, params)
	--[[ transforme un snak en un nouvel objet utilisable par Module:Date complexe
		{type = 'dateobject', timestamp = str, era = '+' ou '-', year = number, month = number, day = number, calendar = calendar}
	]]-- 
	if not params then
		params = {}
	end
	
	local newobj = modules.formatDate.splitDate(orig.time, orig.calendarmodel)
	
	newobj.precision = params.precision or orig.precision
	newobj.type = 'dateobject'
	return newobj
end

local function objectToText(obj, params)
	if obj.type == 'dateobject' then
		return modules.formatDate.simplestring(obj, params)
	elseif obj.type == 'rangeobject' then
		return modules.formatDate.daterange(obj.begin, obj.ending, params)
	end
end

local function getDateFromQualif(statement, qualif)
	if (not statement) or (not statement.qualifiers) or not (statement.qualifiers[qualif]) then
		return nil
	end
	local v = statement.qualifiers[qualif][1]
	if v.snaktype ~= 'value' then -- que faire dans ce cas ?
		return nil
	end
	return dateObject(v.datavalue.value)
end

function wd.getDate(statement)
	local period = getDateFromQualif(statement, 'P585') -- retourne un dateobject
	if period then
		return period
	end
	local begin, ending = getDateFromQualif(statement, 'P580'),  getDateFromQualif(statement, 'P582')
	if begin or ending then
		return rangeObject(begin, ending) -- retourne un rangeobject fait de deux dateobject
	end
end

function wd.getFormattedDate(statement, params)
	if not statement then
		return nil
	end
	local str


	--cherche la date avec les qualifs P580/P582
	local datetable = wd.getDate(statement)
	if datetable then
		str = objectToText(datetable, params)
	end
	
	-- puis limite intérieur / supérieur
	if not str then
		local start, ending = getDateFromQualif(statement, 'P1319'), getDateFromQualif(statement, 'P1326')
		str = modules.formatDate.between(start, ending, params)
	end

	 -- sinon, le mainsnak, pour les données de type time
	if (not str) and (statement.mainsnak.datatype == 'time') then
		local mainsnak = statement.mainsnak
		if (mainsnak.snaktype == 'value') or (mainsnak.snaktype == 'somevalue') then
			str = wd.formatSnak(mainsnak, params)
		end
	end

	if str and params and (params.addstandardqualifs ~= '-') then
		str = wd.addStandardQualifs(str, statement)
	end
	return str
end

-- Fonction qui trie des Claims de type time selon l'ordre chronologique
-- Une clé de tri nomée « dateSortKey » est ajouté à chaque claim.
-- Si des clés de tri de ce nom existent déjà, elles sont utilisées sans modification.
function wd.chronoSort( claims, inverted )
	for _, claim in ipairs( claims ) do
		if not claim.dateSortKey then
			local snack = claim.mainsnak or claim
			local iso
			if (snack.snaktype == 'value') and (snack.datatype == 'time') then
				iso = snack.datavalue.value.time
			else
				iso = timeFromQualifs(claim, datequalifiers) or '0'
			end
			-- transformation en nombre (indication de la base car gsub retourne deux valeurs)
			iso = tonumber( iso:gsub( '(%d)%D', '%1' ), 10 )
			claim.dateSortKey = iso
		end
	end
	table.sort( 
		claims,
		function ( c1, c2 )
			if inverted then
				return c2.dateSortKey < c1.dateSortKey
			end
			return c1.dateSortKey < c2.dateSortKey
		end
	)
	return claims
end


-- ===================
function wd.getReferences(statement)
	local refdata = statement.references
	if not refdata then
		return nil
	end

	local refs = {}
	local hashes = {}
	for i, ref in pairs(refdata) do
		local s
		local function hasValue(prop) -- checks that the prop is here with valid value
			if ref.snaks[prop] and ref.snaks[prop][1].snaktype == 'value' then
				return true
			end
			return false
		end		

		if ref.snaks.P248 then -- cas lorsque P248 (affirmé dans) est utilisé 
			for j, source in pairs(ref.snaks.P248) do
				if source.snaktype == 'value' then
					local page, accessdate, quotation
					if hasValue('P304') then -- page 
						page = wd.formatSnak(ref.snaks.P304[1])
					end
					if hasValue('P813') then -- date de consultation
						accessdate = wd.formatSnak(ref.snaks.P813[1])
					end
					if hasValue('P1683') then -- citation
						quotation = wd.formatSnak(ref.snaks.P1683[1])
					end
					local sourceId = wd.getId(source)
					s = modules.reference.citeitem(sourceId, {['page'] = page, ['accessdate'] = accessdate, ['citation'] = quotation})
					table.insert(refs, s)
					table.insert(hashes, ref.hash .. sourceId)
				end
			end
	
		elseif hasValue('P854') then -- cas lorsque P854 (URL de la référence) est utilisé
			local url, title, author, publisher, accessdate, publishdate, publishlang, quotation

			url = wd.formatSnak(ref.snaks.P854[1], {text = "-"})
			if hasValue('P1476') then
				title = wd.formatSnak(ref.snaks.P1476[1])
			else
				title = mw.ustring.gsub(url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])', 'http%1://%3')
			end

			--todo : handle multiple values for author, etc.
			if hasValue('P813') then -- date de consultation
				accessdate = wd.formatSnak(ref.snaks.P813[1])
			end
			if hasValue('P50') then  -- author (item type)
				author = wd.formatSnak(ref.snaks.P50[1])
			elseif hasValue('P2093') then -- author (string type)
				author = wd.formatSnak(ref.snaks.P2093[1])
			end
			if hasValue('P123') then -- éditeur
				publisher = wd.formatSnak(ref.snaks.P123[1])
			end
			if hasValue('P1683') then -- citation
				quotation = wd.formatSnak(ref.snaks.P1683[1])
			end
			if hasValue('P577') then -- date de publication
				publishdate = wd.formatSnak(ref.snaks.P577[1])
			end 
			if hasValue('P407') then -- langue de l'œuvre
				local id = wd.getId(ref.snaks.P407[1])
				publishlang = getLangCode(id)
			end
			s = modules.cite.lienWeb{titre = title, url = url, auteur = author, editeur = publisher, langue = publishlang, ['en ligne le'] = publishdate, ['consulté le'] = accessdate, ['citation'] = quotation}
			table.insert(hashes, ref.hash)
			table.insert(refs, s)			
		elseif ref.snaks.P854 and ref.snaks.P854[1].snaktype == 'value' then
			s = wd.formatSnak(ref.snaks.P854[1], {text = "-"})
			table.insert(hashes, ref.snaks.P854[1].hash)
			table.insert(refs, s)
		end
	end
	if #refs > 0 then
		if #hashes == #refs then
			return refs, hashes
		end
		return refs
	end
end

function wd.sourceStr(sources, hashes)
	if not sources or (#sources == 0) then
		return nil
	end
	local useHashes = hashes and #hashes == #sources
	for i, j in ipairs(sources) do
		local refArgs = {name = 'ref', content = j}
		if useHashes and hashes[i] ~= '-' then
			refArgs.args = {name = 'wikidata-' .. hashes[i]}
		end
		sources[i] = mw.getCurrentFrame():extensionTag(refArgs)
	end
	return table.concat(sources, '<sup class="reference cite_virgule">,</sup>')
end

function wd.getDataValue(snak, params)
	if not params then
		params = {}
	end
	local speciallabels = params.speciallabels -- parfois on a besoin de faire une liste d'éléments pour lequel le libellé doit être changé, pas très pratique d'utiliser une fonction pour ça

	if snak.snaktype ~= 'value' then
		return nil
	end

	local datatype = snak.datatype
	local value = snak.datavalue.value
	
	local displayformat = params.displayformat
	if type(displayformat) == 'function' then
		return displayformat(snak, params)
	end

	if datatype == 'wikibase-item' then
		return wd.formatEntity(wd.getId(snak), params)
	end

	if datatype == 'url' then
		return modules.weblink.makelink(value, params.text)
	end

	if datatype == 'math' then
		return mw.getCurrentFrame():extensionTag( "math", value)
	end

	if (datatype == 'string') or (datatype == 'external-id') or (datatype == 'commonsMedia') then -- toutes les données de type string sauf "math"
		if params.urlpattern then
			local urlpattern = params.urlpattern
			if type(urlpattern) == 'function' then
				urlpattern = urlpattern(value)
			end
			local url = mw.ustring.gsub(urlpattern, '$1', (value:gsub('%%', '%%%%'))):gsub(' ', '%%20')
			value = '[' .. url .. ' ' .. (params.text or value) .. ']'
		end
		return value
	end
	
	if datatype == 'time' then -- format example: +00000001809-02-12T00:00:00Z
		if displayformat == 'raw' then
			return value.time
		else
			local dateobject = dateObject(value, {precision = params.precision})
			return objectToText(dateobject, params)
		end
	end

	if datatype == 'globe-coordinate' then
		-- retourne une table avec clés latitude, longitude, précision et globe à formater par un autre module (à changer ?)
		if displayformat == 'latitude' then
			return value.latitude
		elseif displayformat == 'longitude' then
			return value.longitude
		else
			local coordvalue = mw.clone( value )
			coordvalue.globe = modules.globes[value.globe] -- transforme l'ID du globe en nom anglais utilisable par geohack
			return coordvalue -- note : les coordonnées Wikidata peuvent être utilisée depuis Module:Coordinates. Faut-il aussi autoriser à appeler Module:Coordiantes ici ?
		end
	end

	if datatype == 'quantity' then -- todo : gérer les paramètres précision
		local amount, unit = value.amount, value.unit

		if unit then
			unit = unit:match('Q%d+')
		end

		local raw	
		if displayformat == "raw" then
			raw = true
		end
		return modules.formatNum.displayvalue(amount, unit,
			{targetunit = params.targetunit, raw = raw, rounding = params.rounding, showunit = params.showunit or 'short', showlink = params.showlink}
		)
	end
	if datatype == 'monolingualtext' then
		if value.language == defaultlang then
			return value.text
		else
			return modules.langmodule.langue({value.language, value.text})
		end
	end	
	return formatError('unknown-datavalue-type' )

end

function wd.stringTable(args) -- like getClaims, but get a list of string rather than a list of snaks, for easier manipulation
	local claims = args.claims
	if not claims then
		claims = wd.getClaims(args)
	end
	if not claims or claims == {} then
		return nil
	end
	local props = {} -- liste des propriétés associété à chaque string pour catégorisation et linkback
	for i, j in pairs(claims) do
		claims[i] = wd.formatStatement(j, args)
		table.insert(props, j.mainsnak.property)
	end
	if args.removedupes and (args.removedupes ~= '-') then
		claims = wd.addNewValues({}, claims) -- devrait aussi supprimer de props celles qui ne sont pas utilisées
	end
	return claims, props
end

function wd.getQualifiers(statement, qualifs, params)
	if not statement.qualifiers then
		return nil
	end
	local vals = {}
	if type(qualifs) == 'string' then
		qualifs = wd.splitStr(qualifs)
	end
	for i, j in pairs(qualifs) do
		if statement.qualifiers[j] then
			for k, l in pairs(statement.qualifiers[j]) do
				table.insert(vals, l)
			end
		end
	end
	if #vals == 0 then
		return nil
	end
	return vals
end

function wd.getFormattedQualifiers(statement, qualifs, params)
	if not params then params = {} end
	local qualiftable = wd.getQualifiers(statement, qualifs)
	if not qualiftable then
		return nil
	end
	qualiftable = wd.filterClaims(qualiftable, params) or {}
	for i, j in pairs(qualiftable) do
		qualiftable[i] = wd.formatSnak(j, params)
	end
	return modules.linguistic.conj(qualiftable, params.conjtype)
end

function wd.showQualifiers(str, statement, args)
	local qualifs =  args.showqualifiers
	if not qualifs then
		return str -- or error ?
	end
	if type(qualifs) == 'string' then
			qualifs = wd.splitStr(qualifs)
	end
	local qualifargs = args.qualifargs or {}
	-- formatage des qualificatifs = args commençant par "qualif", ou à défaut, les mêmes que pour la valeur principale
	qualifargs.displayformat = args.qualifdisplayformat or args.displayformat
	qualifargs.labelformat = args.qualiflabelformat or args.labelformat
	qualifargs.link = args.qualiflink or args.link
	qualifargs.linktopic = args.qualiflinktopic or args.linktopic
	qualifargs.conjtype = args.qualifconjtype
	qualifargs.defaultlink = args.qualifdefaultlink or args.defaultlink
	qualifargs.defaultlinkquery = args.qualifdefaultlinkquery or args.defaultlinkquery
			
	local formattedqualifs = wd.getFormattedQualifiers(statement, qualifs, qualifargs)
	if formattedqualifs and formattedqualifs ~= "" then
		str = str .. " (" .. formattedqualifs .. ")"
	end
	return str
end


function wd.formatSnak( snak, params )
	if not params then params = {} end -- pour faciliter l'appel depuis d'autres modules
	if snak.snaktype == 'somevalue' then
		return unknownValue(snak, params.unknownlabel)
	elseif snak.snaktype == 'novalue' then
		return noValue(params.novaluelabel)
	elseif snak.snaktype == 'value' then
		return wd.getDataValue( snak, params)
	else
		return formatError( 'unknown-snak-type' )
	end
end

function wd.formatStatement( statement, args )
	if not args then
		args = {}
	end
	if not statement.type or statement.type ~= 'statement' then
		return formatError( 'unknown-claim-type' )
	end
	local prop = statement.mainsnak.property

	local str

	-- special displayformat f
	if args.statementformat and (type(args.statementformat) == 'function') then
		str = args.statementformat(statement, args)
	elseif (statement.mainsnak.datatype == 'time') and (statement.mainsnak.dateformat ~= '-') then
		if args.displayformat == 'raw' and statement.mainsnak.snaktype == 'value' then
			str = statement.mainsnak.datavalue.value.time
		else
			str = wd.getFormattedDate(statement, args)
		end
	elseif args.showonlyqualifier and (args.showonlyqualifier ~= '') then
		str = wd.getFormattedQualifiers(statement, args.showonlyqualifier, args)
		if not str then
			return nil
		end
		if args.addstandardqualifs ~= '-' then
			str = wd.addStandardQualifs(str, statement)
		end
	else
		str = wd.formatSnak( statement.mainsnak, args )
		if args.addstandardqualifs ~= '-' then
			str = wd.addStandardQualifs(str, statement)
		end
	end

	-- ajouts divers	
	if args.showlang == true then
		local indicateur = showLang(statement)
		if indicateur then
			str = indicateur .. '&nbsp;' .. str	
		end
	end
	if args.showqualifiers then
		str = wd.showQualifiers(str, statement, args)
	end

	if args.showdate then -- when "showdate and chronosort are both set, date retrieval is performed twice
		local period = wd.getFormattedDate(statement, args, "-") -- 3 arguments indicate the we should not use additional qualifiers, alrady added by wd.formatStatement
		if period then
			str = str .. " <small>(" .. period .. ")</small>"
		end
	end

	if args.showsource then
		local sources, hashes = wd.getReferences(statement)
		if sources then
			local source = wd.sourceStr(sources, hashes)
			if source then
				str = str .. source
			end
		end
	end

	return str
end

function wd.addLinkBack(str, id, property)
	if not id then
		id = wd.getEntityIdForCurrentPage()
	end
	if not id then
		return str
	end
	if type(property) == 'table' then
		property = property[1]
	end
	
	id = wd.entityId(id)

	local class = ''
	if property then
		class = 'wd_' .. string.lower(property)
	end
	local icon = '[[File:Blue pencil.svg|%s|10px|baseline|class=noviewer|link=%s]]'
	local title = wd.translate('see-wikidata-value')
	local url = mw.uri.fullUrl('d:' .. id, 'uselang=fr')
	url.fragment = property -- ajoute une #ancre si paramètre "property" défini
	url = tostring(url)
	local v = mw.html.create('span')
		:addClass(class)
		:wikitext(str)
		:tag('span')
			:addClass('noprint wikidata-linkback')
			:wikitext(icon:format(title, url))
		:allDone()
	return tostring(v)
end

function wd.addRefAnchor(str, id)
--[[
	Insère une ancre pour une référence générée à partir d'un élément wd.
	L'id Wikidata sert d'identifiant à l'ancre, à utiliser dans les modèles type "harvsp"
--]]
	return tostring(
		mw.html.create('span')
			:attr('id', id)
			:attr('class', "ouvrage")
			:wikitext(str)
	)
end

--=== FUNCTIONS USING AN ENTITY AS ARGUMENT ===
local function formatStatementsGrouped(args, type)
	-- regroupe les affirmations ayant la même valeur en mainsnak, mais des qualificatifs différents
	-- (seulement pour les propriétés de type élément)

	local claims = wd.getClaims(args)
	if not claims then
		return nil
	end
	local groupedClaims = {}

	-- regroupe les affirmations par valeur de mainsnak
	local function addClaim(claim)
		local id = wd.getMainId(claim)
		for i, j in pairs(groupedClaims) do 
			if (j.id == id) then
				table.insert(groupedClaims[i].claims, claim)
				return
			end
		end
		table.insert(groupedClaims, {id = id, claims = {claim}})
	end
	for i, claim in pairs(claims) do	
		addClaim(claim)
	end

	local stringTable = {}

	-- instructions ad hoc pour les paramètres concernant la mise en forme d'une déclaration individuelle
	local funs = {
		{param = "showqualifiers", fun = function(str, claims)
			local qualifs = {}
			for i, claim in pairs(claims) do
				local news = wd.getFormattedQualifiers(claim, args.showqualifiers, args)
				if news then
					table.insert(qualifs, news)
				end
			end
			local qualifstr = modules.linguistic.conj(qualifs, "qualif-separator")
			if qualifstr and qualifstr ~= "" then
				str = str .. " (" .. qualifstr .. ")"
			end
			return str
			end
		},
		{param = "showdate", fun = function(str, claims)
			-- toutes les dates sont regroupées à l'intérieur des mêmes parenthèses ex "médaille d'or (1922, 1924)"
			local dates = {}
			for i, statement in pairs(claims) do
				local s = wd.getFormattedDate(statement, args, true)
				if statement then table.insert(dates, s) end
			end
			local datestr = modules.linguistic.conj(dates)
			if datestr and datestr ~= "" then
				str = str .. " <small>(" .. datestr .. ")</small>"
			end
			return str
			end
		},
		{param = "showsource", fun = function(str, claims)
			-- les sources sont toutes affichées au même endroit, à la fin
			-- si deux affirmations ont la même source, on ne l'affiche qu'une fois
			local sources = {}
			local hashes = {}
		
			local function dupeRef(old, new)
				for i, j in pairs(old) do
					if j == new then
						return true
					end
				end
			end
			for i, claim in pairs(claims) do
				local refs, refHashes = wd.getReferences(claim)
				if refs then
					for i, j in pairs(refs) do
						if not dupeRef(sources, j) then
							table.insert(sources, j)
							local hash = (refHashes and refHashes[i]) or '-'
							table.insert(hashes, hash)
						end
					end
				end
			end
			return str .. (wd.sourceStr(sources, hashes) or "")
			end
		}
	}

	for i, group in pairs(groupedClaims) do -- bricolage pour utiliser les arguments de formatStatements
		local str = wd.formatEntity(group.id, args)
		for i, fun in pairs(funs) do
			if args[fun.param] then
				str = fun.fun(str, group.claims, args)
			end
		end
		table.insert(stringTable, str)
	end
				
	args.valuetable = stringTable
	return wd.formatStatements(args)
end


function wd.formatStatements( args )--Format statement and concat them cleanly
	if args.value == '-' then
		return nil
	end

	-- If a value is already set: use it, except if it's the special value {{WD}} (use wikidata)
	if args.value and args.value ~= '' then
		local valueexpl = wd.translate("activate-query")
		if args.value ~= valueexpl then
			return args.value
		end
	-- There is no value set, and args.expl disables wikidata on empty values
	elseif args.expl then
		return nil
	end

	if args.grouped and args.grouped ~= '' then
		args.grouped = false
		return formatStatementsGrouped(args)
	end
	local valuetable = args.valuetable -- dans le cas où les valeurs sont déjà formtées
	local props -- les prorpriétés réellement utilisées (dans certainse cas, ce ne sont pas toutes celles de ags.property
	if not valuetable then -- cas le plus courant
		valuetable, props = wd.stringTable(args)
	end

	local str = modules.linguistic.conj(valuetable, args.conjtype)
	if not str then
		return args.default
	end
	if not props then
		props = wd.splitStr(args.property)[1]
	end
	if args.ucfirst ~= '-' then
		str = modules.linguistic.ucfirst(str)
	end

	if args.addcat and (args.addcat ~= '-') then
		str = str .. wd.addTrackingCat(props)
	end
	if args.linkback and (args.linkback ~= '-') then
		str = wd.addLinkBack(str, args.entity, props)
	end
	if args.returnnumberofvalues then
		return str, #valuetable
	end
	return str
end

function wd.formatAndCat(args)
	if not args then
		return nil
	end
	args.linkback = args.linkback or true
	args.addcat = true
	if args.value then -- do not ignore linkback and addcat, as formatStatements do
		if args.value == '-' then
			return nil
		end
		local val = args.value .. wd.addTrackingCat(args.property)
		val = wd.addLinkBack(val, args.entity, args.property)
		return val
	end 
	return wd.formatStatements( args )
end

function wd.getTheDate(args)
	local claims = wd.getClaims(args)
	if not claims then
		return nil
	end
	local formattedvalues = {}
	for i, j in pairs(claims) do
		local v = wd.getFormattedDate(j, args)
		if v then
			table.insert(formattedvalues, v )
		end
	end
	local val = modules.linguistic.conj(formattedvalues)
	if not val then
		return nil
	end
	if args.addcat == true then
		val = val .. wd.addTrackingCat(args.property)
	end
	val = wd.addLinkBack(val, args.entity, args.property)
	return val
end


function wd.keyDate (event, item, params)
	params = params or {}
	params.entity = item
	if type(event) == 'table' then
		for i, j in pairs(event) do
			params.targetvalue = nil -- réinitialisation barbare des paramètres modifiés
			local s = wd.keyDate(j, item, params)
			if s then
				return s
			end
		end
	elseif type(event) ~= 'string' then
		 return formatError('invalid-datatype', type(event), 'string')
	elseif string.sub(event, 1, 1) == 'Q' then -- on demande un élément utilisé dans P:P793 (événement clé)
		params.property = 'P793'
		params.targetvalue = event
		params.addcat = params.addcat or true
		return wd.getTheDate(params)
	elseif string.sub(event, 1, 1) == 'P'  then -- on demande une propriété
		params.property = event
		return wd.formatAndCat(params)
	else
		return formatError('invalid-entity-id', event)
	end
end

function wd.mainDate(entity)	
	-- essaye P580/P582
	local args = {entity = entity, addcat = true}
	
	args.property = 'P580'
	local startpoint = wd.formatStatements(args)
	args.property = 'P582'
	local endpoint = wd.formatStatements(args)

	local str
	if (startpoint or endpoint) then
		str = modules.formatDate.daterange(startpoint, endpoint, params)
		str = wd.addLinkBack(str, entity, 'P582')
		return str
	end

	-- défaut : P585
	args.property = {'P585', 'P571'}
	args.linkback = true
	return wd.formatStatements(args)
end

-- === FUNCTIONS FOR TRANSITIVE PROPERTIES ===

function wd.getIds(item, query)
	query.excludespecial = true
	query.displayformat = 'raw'
	query.entity = item
	query.addstandardqualifs = '-'
	return wd.stringTable(query)
end


-- recursively adds a list of qid to an existing list, based on the results of a query
function wd.addVals(list, query, maxdepth, maxnodes, stopval)
	maxdepth = tonumber(maxdepth) or 10
	maxnodes = tonumber(maxnodes) or 100
	if (maxdepth < 0) then
		return list
	end
	if stopval and wd.isHere(list, stopval) then
		return list
	end
	local origsize = #list
	for i = 1, origsize do
		-- tried a  "checkpos" param instead of starting to 1 each time, but no impact on performance
		local candidates = wd.getIds(list[i], query)
		list = wd.addNewValues(list, candidates, maxnodes, stopval)
		if list[#list] == stopval then
			return list
		end
		if #list >= maxnodes then
			return list
		end
	

end
	if (#list == origsize) then
		return list
	end
	return wd.addVals(list, query, maxdepth - 1, maxnodes, stopval, origsize + 1)
end

-- returns a list of items transitively matching a query (orig item is not included in the list)

function wd.transitiveVals(item, query, maxdepth, maxnodes, stopval)
	maxdepth = tonumber(maxdepth) or 5
	if type(query) == "string" then
		query = {property = query}
	end

	-- récupération des valeurs
	local vals = wd.getIds(item, query)
	if not vals then
		return nil
	end
	local v = wd.addVals(vals, query, maxdepth - 1, maxnodes, stopval) 
	if not v then
		return nil
	end

	-- réarrangement des valeurs
	if query.valorder == "inverted" then
		local a = {}
		for i, j in pairs(v) do
			table.insert(a, 1, j)
		end
		v = a
	end

	return v
end

-- returns true if an item is the value of a query, transitively
function wd.inTransitiveVals(searchedval, sourceval, query, maxdepth, maxnodes )
	local vals = wd.transitiveVals(sourceval, query, maxdepth, maxnodes, searchedval )
	if (not vals) then 
		return false
	end
	for _, val in ipairs(vals) do
		if (val == searchedval) then
			return true
		end
	end
	return false
end

-- returns true if an item is a superclass of another, based on P279
function wd.isSubclass(class, item, maxdepth)
	local query = {property = 'P279'}
	if class == item then -- item is a subclass of itself iff it is a class
		if wd.getIds(item, query) then
			return true
		end
		return false
	end
	return wd.inTransitiveVals(class, item, query, maxdepth )
end

-- returns true if one of the best ranked P31 values of an item is the target or a subclass of the target
-- rank = 'valid' would seem to make sense, but it would need to check for date qualifiers as some P31 values have begin or end date
function wd.isInstance(targetclass, item, maxdepth)
	maxdepth = maxdepth or 10
	local directclasses = wd.transitiveVals(item, {property = 'P31'}, 1)
	if not directclasses then
		return false
	end 
	for i, class in pairs(directclasses) do
		if wd.isSubclass(targetclass, class, maxdepth - 1) then
			return true
		end
	end
	return false
end

-- return the first value in a transitive query that belongs to a particular class. For instance find a value of P131 that is a province of Canada
function wd.findVal(sourceitem, targetclass, query, recursion, instancedepth)
	if type(query) == "string" then
		query = {property = query}
	end
	local candidates = wd.getIds(sourceitem, query)
	if candidates then
		for i, j in pairs(candidates) do
			if wd.isInstance(targetclass, j,  instancedepth) then
				return j
			end
		end
		if not recursion then
			recursion = 3
		else
			recursion = recursion - 1
		end
		if recursion < 0 then
			return nil
		end
		for i, candidate in pairs(candidates) do
			return wd.findVal(candidate, targetclass, query, recursion, instancedepth)
		end
	end
end


-- === VARIA ===
function wd.getDescription(entity, lang)
	lang = lang or defaultlang

	local description
	if lang == defaultlang then
		return  mw.wikibase.descriptionl(qid)
	end
	if not entity.descriptions then
		return wd.translate('no description')
	end
	local descriptions = entity.descriptions
	if not descriptions then
		return nil
	end
	if descriptions[lang] then
		return descriptions[delang].value
	end
	return entity.id
end


function wd.Dump(entity)
	entity = wd.getEntity(entity)
	if not entity then
		return formatError("entity-param-not-provided")
	end
	return "<pre>"..mw.dumpObject(entity).."</pre>"
end

function wd.frameFun(frame)
	local args = frame.args
	local funname = args[1]
	table.remove(args, 1)
	return wd[funname](args)
end


return wd