--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 = 'Módulo:Wikidata/Referências',
    i18n = 'Módulo:Wikidata/i18n',
    globes = 'Módulo:Wikidata/Globes',
    langcodes = 'Módulo:Dicionário Wikidata/Códigos língua', -- big, infrequently used data
    invertedlangcodes = 'Módulo:Dicionário Wikidata/Códigos língua/inverso',

    linguistic = 'Módulo:Linguística',
    formatDate = 'Módulo:Data complexa',
    formatNum = 'Módulo:Conversão',
    langmodule = 'Módulo:Língua',
    cite = 'Módulo:Citação/CS1',
    weblink = 'Módulo: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
    end
    return entity.id
end

--[[
This is used to return an image legend from Wikidata
image is property P18
image legend is property P2096

Call as {{#invoke:Infobox/Wikidata |getImageLegend | <PARAMETER> | lang=<ISO-639code> |id=<QID>}}
Returns PARAMETER, unless it is equal to "FETCH_WIKIDATA", from Item QID (expensive call)
If QID is omitted or blank, the current article is used (not an expensive call)
If lang is omitted, it uses the local wiki language, otherwise it uses the provided ISO-639 language code
ISO-639: https://docs.oracle.com/cd/E13214_01/wli/docs92/xref/xqisocodes.html#wp1252447

Ranks are: 'preferred' > 'normal'
This returns the label from the first image with 'preferred' rank
Or the label from the first image with 'normal' rank if preferred returns nothing
Ranks: https://www.mediawiki.org/wiki/Extension:Wikibase_Client/Lua
]]

wd.getImageLegend = function(frame)
    -- look for named parameter id; if it's blank make it nil
    local id = frame.args.id
    if id and (#id == 0) then
        id = nil
    end

    -- look for named parameter lang
    -- it should contain a two-character ISO-639 language code
    -- if it's blank fetch the language of the local wiki
    local lang = frame.args.lang
    if (not lang) or (#lang < 2) then
        lang = mw.language.getContentLanguage().code
    end

    -- first unnamed parameter is the local parameter, if supplied
    local input_parm = mw.text.trim(frame.args[1] or "")
    if input_parm == "FETCH_WIKIDATA" then
        local ent = mw.wikibase.getEntityObject(id)
        local imgs
        if ent and ent.claims then
            imgs = ent.claims.P18
        end
        local imglbl
        if imgs then
            -- look for an image with 'preferred' rank
            for k1, v1 in pairs(imgs) do
                if v1.rank == "preferred" and v1.qualifiers and v1.qualifiers.P2096 then
                    local imglbls = v1.qualifiers.P2096
                    for k2, v2 in pairs(imglbls) do
                        if v2.datavalue.value.language == lang then
                            imglbl = v2.datavalue.value.text
                            break
                        end
                    end
                end
            end
            -- if we don't find one, look for an image with 'normal' rank
            if (not imglbl) then
                for k1, v1 in pairs(imgs) do
                    if v1.rank == "normal" and v1.qualifiers and v1.qualifiers.P2096 then
                        local imglbls = v1.qualifiers.P2096
                        for k2, v2 in pairs(imglbls) do
                            if v2.datavalue.value.language == lang then
                                imglbl = v2.datavalue.value.text
                                break
                            end
                        end
                    end
                end
            end
        end
        return imglbl
    else
        return input_parm
    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 entity_on_its_page = false
    local self_entity = mw.wikibase.getEntity()
    if self_entity ~= nil and qid == wd.entityId(self_entity) then
        entity_on_its_page = true
    end
    return entity_on_its_page
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) -- converte em strings strings do Wikitexto que usam vírgulas de separação
    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 -- evite carregar o elemento inteiro
        return  mw.wikibase.sitelink(entity), 'wiki', defaultlang
    end
    if project == 'wikidata' then
        return wikidataLink(entity), 'wikidata'
    end
    local projects = {
        -- nome = {prefixo no Wikidata, prefixo de links na Wikipedia, adicionar prefixo de idioma}
        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}
    }

    entity = wd.getEntity(entity)
    if not entity then
        return nil
    end

    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 = entity:getSitelink(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 snack = claim.mainsnak or snack
    return snack.snaktype == 'value'
end

local function hasTargetValue(claim, targets) -- retorna verdadeiro se o valor estiver na lista de destino ou se for um valor especial filtrado separadamente por excluiespecial
    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) -- se o valor não estiver na lista ou se for um valor especial (filtrado separadamente por excludeespecial)
    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) -- verificação para um qualificador individual
        if not claimqualifs[qualif] then
            return false
        end
        if not (acceptedvals) then  -- se nenhum valor específico for solicitado, 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] -- snaks usando a propriedade solicitada
    if (not candidates) then
        return false
    end
    if (targetsource == "any") then -- se algum valor for aceite, desde que use a propriedade solicitada
        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 -- não excluir valores especiais, há uma função dedicada para isso
        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) -- funciona apenas para texto monolingual / estender para outros tipos usando qualificadores?
    if type(lang) == 'table' then -- se é uma tabela de idiomas separada por vírgulas, aceitamos todas
        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

    -- para outros tipos de dados: pesquisa em qualificadores
    if (lang == 'pt') then
        lang = 'Q5146'
    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 -- se não conhecemos a língua, condicção que é bom
end

local function firstVals(claims, numval) -- retornar os primeiros valores numéricos da tabela de declarações
    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 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 -- se uma data é dada como um qualificador, é aquela que é usada preferencialmente em handsnak
        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) --remova das tabelas de reclamação aquelas removidas por um dos filtros na tabela de filtros

    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.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
  
    if (type(entity) == 'string') and (lang == defaultlang) then -- o mais economico
        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

    if type(entity) == 'string' then
        entity = mw.wikibase.getEntity(entity)
    end
  
    if entity and entity.labels and entity.labels[lang] then
        return entity.labels[lang].value, true
    end
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 params.idioma 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
    local str = '' --  o texto completo a devolver  
  
  
    label = wd.getLabel(entity, lang, labelformat)
  
    -- determinar se o item está ou não sendo renderizado na página de seu artigo
    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 str .. '[[' .. link .. '|' .. id .. ']]' .. addCat(modules.i18n['to translate'])
-- se não houver rótulo, colocamos um link para o Wikidata para entender o que ele se refere
    end

    if (linktype == '-') or rendering_entity_on_its_page then
        return str .. 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 str .. '[[' .. 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 str  .. label .. '<small>&nbsp;(' .. val .. ')</small>'
    end
    return str .. label
end


function wd.addTrackingCat(prop, cat) -- às vezes deve ser chamado por outros módulos
    if type(prop) == 'table' then
        prop = prop[1] -- deve logicamente adicioná-los todos
    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) -- retorna o código do idioma entre parênteses antes do valor (por exemplo, para bibliotecas e links externos)
    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 -- se é em português, não precisa dizer
        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)
    --[[
        objeto com um timestamp para a classificação cronológica e dois objetos date (inicial e final)
    ]]--
    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)
    --[[ transforma um snak em um novo objeto utilizável pelo módulo: data complexa
        {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 fazer neste caso ?
        return nil
    end
    return dateObject(v.datavalue.value)
end

function wd.getDate(statement)
    local period = getDateFromQualif(statement, 'P585') -- devolver um 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


    --procure a data com os qualificadores P580/P582
    local datetable = wd.getDate(statement)
    if datetable then
        str = objectToText(datetable, params)
    end
  
    -- se estão no limite interno / superior
    if not str then
        local start, ending = getDateFromQualif(statement, 'P1319'), getDateFromQualif(statement, 'P1326')
        str = modules.formatDate.between(start, ending, params)
    end

     -- caso contrário, o handsnak, para os dados do tipo de tempo
    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

-- Função que ordena as reivindicações de tempo em ordem cronológica
-- Uma chave de classificação chamada "dateSortKey" é adicionada a cada declaração.
-- Se as chaves de classificação desse nome já existirem, elas serão usadas sem modificação.
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
            -- transformação em número (indicação da base porque gsub retorna dois valores)
            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 = {}
    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
            for j, source in pairs(ref.snaks.P248) do
                if source.snaktype == 'value' then
                    local page, accessdate
                    if hasValue('P304') then
                        page = wd.formatSnak(ref.snaks.P304[1])
                    end
                    if hasValue('P813') then
                        accessdate = wd.formatSnak(ref.snaks.P813[1])
                    end
                    s = modules.reference.citeitem(wd.getId(source), {['page'] = page, ['accessdate'] = accessdate})
                    table.insert(refs, s)
                end
            end
  
        elseif hasValue('P854') then -- P854: reference url
            local url, title, author, publisher, accessdate, publishdate, publishlang

            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
                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
                publisher = wd.formatSnak(ref.snaks.P123[1])
            end
            -- publishdate avec P577 ? (ne semble pas vraiment correspondre)
            if hasValue('P407') then
                local id = wd.getId(ref.snaks.P407[1])
                publishlang = getLangCode(id)
            end
            s = '' --modules.reference.citeitem(wd.getId(source))
            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(refs, s)
        end
    end
    if #refs > 0 then
        return refs
    end
end

function wd.sourceStr(sources)
    if not sources or (#sources == 0) then
        return nil
    end
    for i, j in pairs(sources) do
        sources[i] = mw.getCurrentFrame():extensionTag( "ref", j)
    end
        return table.concat(sources, '<sup class="reference" style= "padding-left: 0;    padding-right: 1px;">,</sup>')
end

function wd.getDataValue(snak, params)
    if not params then
        params = {}
    end
    local speciallabels = params.speciallabels -- às vezes precisamos fazer uma lista de elementos para os quais o rótulo deve ser alterado, não é muito prático usar uma função para isso

    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 -- todos os dados da string, exceto "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.texto 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
        -- retorna uma tabela com as chaves latitude, longitude, precisão e globo para formatar por outro módulo (para alterar?)
        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] -- transforma o globo ID em nome inglês utilizável por geohack
            return coordvalue --nota : As coordenadas do Wikidata podem ser usadas em Módulo:Coordenadas. Devemos também permitir chamar Módulo:Coordenadores aqui?
        end
    end

    if datatype == 'quantity' then -- todo : gerenciar os parâmetros precisão
        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.lingua({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 = {} -- lista de propriedades associadas a cada string para categorização e 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) -- também deve remover dos adereços aqueles que não são usados
    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 {}
    -- qualificadores de formatação = args começando com "qual", ou alternativamente, o mesmo que para o valor principal
    qualifargs.displayformat = args.qualifdisplayformat or args.displayformat
    qualifargs.labelformat = args.qualiflabelformat or args.labelformat
    qualifargs.link = args.qualiflink or args.link
    qualifargs.conjtype = args.qualifconjtype
          
    local formattedqualifs = wd.getFormattedQualifiers(statement, qualifs, qualifargs)
    if formattedqualifs and str then
        str = str .. modules.linguistic.inparentheses(formattedqualifs, defaultlang)
    end
    return str
end


function wd.formatSnak( snak, params )
    if not params then params = {} end -- para facilitar a chamada de outros módulos
    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 = wd.getReferences(statement)
        if sources then
            local source = wd.sourceStr(sources)
            str = str .. (source or "")
        end
    end

    return str
end

function wd.addLinkBack(str, id, property)
    if not id then
        id = wd.getEntity()
    end
    if not id then
        return str
    end
    if type(property) == 'table' then
        property = property[1]
    end
    if type(id) == 'table' then
        id = id.id
    end
  
    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=pt')
    url.fragment = property -- adicionar um parâmetro #âncora if "property" definido
    url = tostring(url)
    local v = mw.html.create('span')
        :addClass(class)
        :wikitext(str)
        :tag('span')
            :addClass('noprint wikidata-linkback')
            :css('padding-left', '0.5em')
            :wikitext(icon:format(title, url))
        :allDone()
    return tostring(v)
end

function wd.addRefAnchor(str, id)
--[[
    Insere uma âncora para uma referência gerada a partir de um elemento wd.
         O id Wikidata serve como um identificador para a âncora, para usar
    em modelos como "harvsp"
--]]
    return tostring(
        mw.html.create('span')
            :attr('id', id)
            :attr('class', "livro")
            :wikitext(str)
    )
end

--=== FUNCTIONS USING AN ENTITY AS ARGUMENT ===
local function formatStatementsGrouped(args, type)
          -- grupos afirmações com o mesmo valor em handsnak, mas diferentes qualificadores
          -- (somente para propriedades de tipo de elemento)

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

    -- afirmações de grupo pelo valor 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 = {}

    -- instruções ad hoc para configurações para formatar um retorno individual
    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 not qualifstr then
                return str
            end
            return str .. " " .. modules.linguistic.inparentheses(qualifstr)
            end
        },
        {param = "showdate", fun = function(str, claims)
            -- todas as datas são agrupadas dentro dos mesmos parênteses ex "medalha de ouro (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 not datestr then
                return str
            end
            return str .. "<small>" .. modules.linguistic.inparentheses(datestr) .. "</small>"
            end
        },
        {param = "showsource", fun = function(str, claims)
            -- as fontes são todas exibidas no mesmo lugar, no final
            -- se duas instruções tiverem a mesma fonte, ela será exibida apenas uma vez
            local sources = {}
      
            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 = wd.getReferences(claim)
                if refs then
                    for i, j in pairs(refs) do
                        if not dupeRef(sources, j) then
                            table.insert(sources, j)
                        end
                    end
                end
            end
            return str .. (wd.sourceStr(sources) or "")
            end
        }
    }

    for i, group in pairs(groupedClaims) do -- bricolage para usar os argumentos 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
    local valueexpl = wd.translate("activate-query")
    --If a value is already set, use it
    if args.value and (args.value ~= '') and (args.value ~= valueexpl) then
        return args.value
    end
    -- if args.expl: return something only one if explicitly required in Wikitext
    if args.expl and (args.value ~= valueexpl) then
        return nil
    end
    args.entity = wd.getEntity(args.entity)

    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 -- as propriedades realmente usadas (em alguns casos, nem todas de args.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 nil
    end
    if not props then
        props = wd.splitStr(args.property)[1]
    end
    if args.ucfirst == '-' then
        str = modules.linguistic.lcfirst(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 -- reinicialização bárbara dos parâmetros modificados
            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 -- pedimos um elemento usado em P: P793 (evento chave)
        params.property = 'P793'
        params.targetvalue = event
        params.addcat = params.addcat or true
        return wd.getTheDate(params)
    elseif string.sub(event, 1, 1) == 'P'  then -- pedimos uma propriedade
        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