local mWikidata = require('Modulo:Wikidata')
local getArgs = require('Modulo:Arguments').getArgs
local conf = mw.loadData('Modulo:Software/Configurazione')
local p = {}

--------------------------------------------------------------------------------

local UNKNOWN       = -1 -- Non è possibile identificare la licenza
local FREE          =  1 -- È una licenza di software libero
local PROPRIETARY   =  2 -- È una licenza di software proprietario

local MAX_RECURSION =  4 -- [[Modulo:Software/man#Considerazioni implementative]]

local UNKNOWN_WD_LANGUAGE = "[[Categoria:Linguaggio di programmazione da Wikidata non previsto]]"
local UNKNOWN_WD_LICENSE  = ""
local UNKNOWN_WD_TOOLKIT  = "[[Categoria:Toolkit o framework dell'interfaccia grafica non previsto]]"

local TEMPLATE_UNKNOWN_ARG_WARN = "&nbsp;<span style=\"font-size:75%\">(non&nbsp;in&nbsp;[[Template:Software#Linguaggi|lista]])</span>"
local TEMPLATE_UNKNOWN_ARG_CAT  = "[[Categoria:Software in linguaggio non riconosciuto]]"

local LICENSE_CAT         = "Software con licenza %s"
--                          "Software con licenza GNU GPL"

local LICENSE_CAT_SHORTER = "Software %s"
--                          "Software freeware"

local YEAR_CAT = 'Software del %d'

-------------------------------- Sgabuzzino ------------------------------------

local function rawProperty(property)
	return mWikidata._formatStatement(property, {formatting = 'raw'})
end

--[[
* @param string Wikidata element
* @TODO ora prende il primo, a prescindere dalla lingua (credo).
* @return string
]]
local function shortWikidataLabel(from)
	local s = ''
	local label    = mw.wikibase.label(from)
	local sitelink = mw.wikibase.sitelink(from) or label
	if string.len(label) > 10 then
		label = mWikidata._getProperty( { 'P1813', n = 1, from = from } ) or label
	end
	return "[[" .. sitelink .. "|" .. label .. "]]"
end


--[[
* @param v bool
* @return string
]]
local function yesNo(v)
	return v and 'sì' or 'no'
end

--[[
* @param v bool
* @return string|nil
]]
local function yesNoNil(v)
	if v == nil then
		return nil
	end
	return yesNo(v)
end


--[[
* Poi non ditemi che PHP fa schifo. asd.
* Sembra che l'operatore "#" ogni tanto non vada col Modulo:Wikidata.
* 00:28, 28 feb 2017‎ Valerio Bozzolan
]]
local function count(t)
	local i = 0
	for _,_ in pairs(t) do
		i = i + 1
	end
	return i
end

--[[
* Restituisce una tabella completa di proprietà non formattate.
* @return {}|nil
* ]]
local function rawProperties(from, property)
	return mWikidata._getProperty({property, from = from, formatting = 'raw'}, true)
end

local function onlySoftwareArguments(frame)
	return getArgs(frame, {wrappers = 'Template:Software'} )
end

--[[
* Restituisce SOLO l'argomento `from` da dare in pasto a varie funzioni del Modulo:Wikidata
* @param frame table
* @return string|nil
]]
local function fromItem(frame)
	return getArgs(frame).from
end

--[[
* Analogo a fromItem() ma viene recuperato da Property:P301 (category's main topic).
*
* @return string|nil
]]
local function fromItemMainTopic()
	local from = mWikidata._getClaims('P301')
	from = from and from[1]
	if not from then
		error("Questa non è una categoria, o è assente la proprietà Wikidata P301")
	end
	return rawProperty(from)
end

--[[
* Formatta una categoria.
*
* @param category string Primo argomento per sprinf
* @param part string Placeholder per sprintf
* @param man boolean Mostrare solo la categoria invece che categorizzare?
* @return string
]]
local function formatCategory(category, part, man)
	local colon = man and ':' or ''
	return "[[" .. colon .. "Categoria:" .. string.format(category, part) .. "]]"
end

--[[
* Categorie di errore nel caso non ci siano linguaggi.
*
* @param free boolean|nil È software libero?
]]
local function noLanguageWarning(free)
	return free and '[[Categoria:Software libero senza linguaggio]]' or '[[Categoria:Software senza linguaggio]]'
end

------------------------------- Frontend ---------------------------------------

function p.shouldHaveALanguage(frame)
	return yesNo( p._shouldHaveALanguage( onlySoftwareArguments(frame) ) )
end

function p.specifiesALanguage(frame)
	return yesNo( p._specifiesALanguage( onlySoftwareArguments(frame) ) )
end

function p.specifiesAToolkit(frame)
	return yesNo( mWikidata._getClaims('P277') )
end

function p.isFreeSoftware(frame)
	return yesNoNil( p._isFreeSoftware( onlySoftwareArguments(frame) ) )
end

function p.hasAFreeLicense(frame)
	return yesNoNil( p._hasAFreeLicense( fromItem(frame) ) )
end

function p.hasAProprietaryLicense(frame)
	return yesNoNil( p._hasAProprietaryLicense( fromItem(frame) ) )
end

--[[
* Categorizza una voce.
* @return string|nil
]]
function p.categories(frame)
	if p._categorize( onlySoftwareArguments(frame) ) then
		return p.licenseCategories(frame) .. p.toolkitCategories(frame) .. p.yearsCategories(frame)
	end
	return nil
end

function p.wikidataCategoriesFromMainTopic(frame)
	local s = p._wikidataCategories( fromItemMainTopic() )
	return s .. string.format('[[Categoria:P301 %s Wikidata]]', s and 'letta da' or 'assente su')
end

--[[
* Categorizza una pagina qualsiasi.
* @return string|nil
]]
function p.wikidataCategories(frame)
	return p._wikidataCategories( fromItem(frame) ) or nil
end

--[[
* Tutte le categorie applicabili da Wikidata.
*
* Per ora ci sono solo le licenze, ma qui ci devono andare anche i linguaggi.
*
* @param from string|nil Wikidata Item
* @return string
]]
function p._wikidataCategories(from)
	return p._licenseCategories(from) .. p._languageCategories(from) .. p._toolkitCategories(from) .. p._yearsCategories(from)
end

--[[
* Categorie legate alle licenze.
* @return string
]]
function p.licenseCategories(frame)
	return p._licenseCategories( fromItem(frame) )
end

--[[
* Categorie legate al toolkit.
* @return string
]]
function p.toolkitCategories(frame)
	return p._toolkitCategories( fromItem(frame) )
end

--[[
* Categoria legato all'anno di fondazione.
* @return string
]]
function p.yearsCategories(frame)
	return p._yearsCategories( fromItem(frame), onlySoftwareArguments(frame).DataPrimaVersione )
end

--[[
* Linguaggi di programmazione.
*
* @return string
]]
function p.languages(frame)
	local s = ''

	local args = frame and onlySoftwareArguments(frame)
	local categorize    = p._categorize(args)
	local is_free       = p._isFreeSoftware(args)
	local tl_has        = p._templateHasLanguages(args) and true or false --exclude nil
	local tl_languages  = p._getTemplateLanguages(args)
	local wd_languages  = mWikidata._getClaims('P277')

	-- È il namespace principale?
	local nszero = mw.title.getCurrentTitle().namespace == 0

	-- Il template si è arricchito grazie a Wikidata?
	local improved = false

	-- Tutti i linguaggi sono identificati da Wikidata e dal template?
	local tl_allFound = true
	local wd_allFound = true

	-- Il template non mostra alcun linguaggio?
	local noLanguages = not tl_has

	local outputLanguages = {} -- {key = {label1, category1}, key = {label2, category2}}
	local outputLanguage = function(label, category, note)
		return {label = label, category = category, note = note or '' }
	end

	if wd_languages then
		for i, language in pairs(wd_languages) do
			local languageId = rawProperty(language)
			      language   = conf.language[languageId]
			if language then
				outputLanguages[languageId] = outputLanguage(
					shortWikidataLabel(languageId),
					p._languageCategory(language, is_free)
				)
				noLanguages = false
				improved = true
			else
				wd_allFound = false
			end
		end

		if nszero then
			if improved then
				if tl_has then
					-- Wikidata ha migliorato il template e il template aveva già altri valori
					s = s .. '[[Categoria:P277 differente su Wikidata]]'
				else
					-- Wikidata ha migliorato il template che era vuoto
					s = s .. '[[Categoria:P277 letta da Wikidata]]'
				end
			else
				if p._templateHasExtraInformations(args) or count(tl_languages) > count(wd_languages) then
					-- Nel template c'è qualcosa in più rispetto a Wikidata
					s = s .. '[[Categoria:P277 differente su Wikidata]]'
				elseif count(wd_languages) ~= 0 then
					-- Non ha migliorato niente perchè sono gli stessi valori
					s = s .. '[[Categoria:P277 uguale su Wikidata]]'
				end
			end
		end
	else
		if tl_has and nszero then
			-- solo se il template specifica linguaggi ma Wikidata no
			s = s .. '[[Categoria:P277 assente su Wikidata]]'
		end
	end

	if tl_languages then
		for languageSlug, note in pairs(tl_languages) do
			if languageSlug == 'sconosciuto' then
				outputLanguages[languageSlug] = outputLanguage(
					"Sconosciuto",
					TEMPLATE_UNKNOWN_ARG_CAT,
					note
				)
			else
				local languageId = conf.languageSlugToWikidata[languageSlug]
				local language   = conf.language[languageId]
				if language then
					outputLanguages[languageId] = outputLanguage(
						shortWikidataLabel(languageId),
						p._languageCategory(language, is_free),
						note
					)
				else
					-- Mostrala comunque, a caso, così, tanto per
					outputLanguages[languageSlug] = outputLanguage(
						languageSlug,
						p._languageCategory(languageSlug, is_free),
						note
					)
					tl_allFound = false
				end
			end
		end
	end

	local i = 0
	for id, language in pairs(outputLanguages) do
		local glue = i > 0 and '<br />' or ''
		s = s .. glue .. language.label .. language.note .. (categorize and language.category or '')
		i = i + 1
	end

	if args['LinguaggioAltri'] then
		local glue = i > 0 and '<br />' or ''
		s = s .. glue .. args['LinguaggioAltri']
	end

	if nszero then
		if not tl_allFound then
			s = s .. TEMPLATE_UNKNOWN_ARG_WARN .. TEMPLATE_UNKNOWN_ARG_CAT
		end
		if not wd_allFound then
			s = s .. UNKNOWN_WD_LANGUAGE
		end
		if noLanguages then
			s = s .. noLanguageWarning(is_free)
		end
	end

	return s
end

function p.manLanguages(frame)
	local s  = '<table class="wikitable"><tr><th>Parametro</th><th>Risultato</th><th>Categoria automatica</th></tr>'
	local TD = '<td>%s</td>'
	for id, language in pairs( conf.language ) do
		s = s .. '<tr>'
		local slugs, i = '', 0
		for slug, sub_id in pairs( conf.languageSlugToWikidata ) do
			if sub_id == id then
				local glue = i > 0 and '<br />' or ''
				slug  = "<code>" .. slug .. "</code>"
				slugs = slugs .. glue .. slug
				i = i + 1
			end
		end
		s = s .. string.format(TD, slugs)
		s = s .. string.format(TD, shortWikidataLabel(id) )
		s = s .. string.format(TD, p._languageCategory(language, nil, true, '//') )
		s = s .. '</tr>'
	end
	return s .. '</table>'
end

-------------------------------- Backend ---------------------------------------

--[[
* Categorizzare?
*
* @return true|false
]]
function p._categorize(args)
	local v = args['Categorie']
	if v then
		return mw.ustring.lower(v) ~= 'no'
	end
	return true
end

--[[
* Riassumendo. La voce è software libero?
*
* @return true|false|nil
]]
function p._isFreeSoftware(args)
	local is_wd   = p._isFreeSoftwareByWikidata()
	local isnt_wd = p._isProprietarySoftwareByWikidata()
	local is_tp   = p._isFreeSoftwareByTemplate(args)

	local is = nil
	if is_wd then
		is = true
	end
	if isnt_wd then
		is = false
	end
	if is_tp ~= nil then
		is = is_tp
	end

	--if is_wd == true and isnt_wd == true then
		-- Wikidata nonsense
	--end

	--if is_wd == true and is_tp == true then
		-- Local same as Wikidata
	--

	--if (is_wd == true and is_tp == false) or (isnt_wd == true and is_tp == false) then
		-- Free different from Wikidata
	--

	--if (is_wd ~= nil or isnt_wd ~= nil) and is_tp == false then
		-- Free/proprietary read from Wikidata
	--
	return is
end

--[[
* Spesso la voce è definita in Wikidata come istanza di software libero.
* Per quanto riguarda Wikipedia è comodo.
* Per quanto riguarda Wikidata è ridondante col campo licenza P275.
*
* @param string from Wikidata item
* @return true|false
]]
function p._isFreeSoftwareByWikidata(from)
	-- Q341 free software
	-- Q1130645 open-source software
	return mWikidata._instanceOf({'Q341', 'Q1130645', from = from}) or p._hasAFreeLicense(from)
end

--[[
* Spesso la voce è definita in Wikidata come istanza di software proprietario.
* Per quanto riguarda Wikipedia è comodo.
* Per quanto riguarda Wikidata è ridondante col campo licenza P275.
*
* @param from string Wikidata item
* @return true|false
]]
function p._isProprietarySoftwareByWikidata(from)
	-- Q218616 proprietary software
	-- Q178285 freeware TODO: impropria, subclass of precedente
	return mWikidata._instanceOf({'Q218616', 'Q178285', from = from}) or p._hasAProprietaryLicense(from)
end

--[[
* Banale lettura di `SoftwareLibero = sì/no` nel template {{Software}}
*
* @return nil|true|false
--]]
function p._isFreeSoftwareByTemplate(args)
	local is = nil
	local v = args['SoftwareLibero']
	if v then
		local yep = {['sì'] = true, ['si'] = true, ['no'] = false}
		v = yep[ mw.ustring.lower(v) ]
	end
	return v
end

--[[
* Riassumendo. La voce dovrebbe specificare un linguaggio?
*
* @return true|nil
]]
function p._shouldHaveALanguage(args)
	return p._isFreeSoftware(args) or p._specifiesALanguage(args)
end

--[[
* C'è un linguaggio?
*
* @return truly|nil
]]
function p._specifiesALanguage(args)
	return p._templateHasLanguages(args) or mWikidata._getClaims('P277')
end

--[[
* Il template ha il campo "Linguaggio"?
*
* @return truly|nil
]]
function p._templateHasLanguages(args)
	return args['Linguaggio'] or args['NotaLinguaggio'] or args['LinguaggioAltri']
end

--[[
* Il template ha valori non unibili a Wikidata?
*
* @return truly|nil
]]
function p._templateHasExtraInformations(args)
	return args['LinguaggioAltri'] or args['NotaLinguaggio'] or args['NotaLinguaggio2'] or args['NotaLinguaggio3']
end

--[[
* Per mantenere retrocompatibilità con la possibilità di avere più codici.
*
* Esempio:
* 'cpp' → 'c++'
* 'c++' → 'c++'
*
* @param string
* @see Template:Software/Linguaggio
* @return string
]]
function p._preferredLanguageSlug(slug)
	if slug then
		slug = string.lower(slug)
	end
	return conf.languageSlugAlias[slug] or slug
end

--[[
* I codici dei linguaggi del template {{Software}}.
*
* @return string[] = 'c' => 'nota linguaggio c', 'c++' = '', ..
]]
function p._getTemplateLanguages(args)
	local languages = {}
	for i=0,3 do
		local j = i == 0 and '' or i
		local v = args['Linguaggio' .. j]
		if v then
			v = p._preferredLanguageSlug(v)
			languages[ v ] = args['NotaLinguaggio' .. j] or ''
		end
	end
	return languages
end

--[[
* Questa licenza è direttamente identificabile?
*
* @param license string Wikidata item
* @return FREE|PROPRIETARY|UNKNOWN
]]
function p._singleLicenseType(license)
	-- Q3943414  free software license
	-- Q31202214 proprietary software license
	-- Q218616   proprietary software
	-- Q3238057  proprietary license!

	--[[
	* Approfonditi studi da punti di vista non neutrali hanno evidenziato come
	* le prossime tre righe riassumino l'*unica* parte piacevole del Lua.
	]]
	return license == 'Q3943414'                                                    and FREE
	   or (license == 'Q31202214' or license == 'Q218616' or license == 'Q3238057') and PROPRIETARY
	   or                                                                           UNKNOWN
end

--[[
* Fra queste licenze c'è una classe radice identificabile?
*
* @param license string Wikidata item
* @return FREE|PROPRIETARY|UNKNOWN
* ]]
function p._findLicenseType(licenses)
	local type = UNKNOWN
	for _, license in pairs(licenses) do
		type = p._singleLicenseType(license)
		if type ~= UNKNOWN then
			break
		end
	end	
	return type
end

--[[
* Ottiene la tipologia di una qualsiasi licenza software.
*
* Una licenza è identificabile grazie a Wikidata scorrendo ricorsivamente
* l'albero di licenze madri fino ad arrivare ad una classe radice identificabile.
*
* Questo può sembrare spaventoso, in verità sembra piuttosto efficiente,
* portando l'utilizzo medio di funzioni parser dispendiose a +2/+3.
*
* In ogni caso il limite delle funzioni parser dispendiose è 500,
* mentre si ipotizza che questa funzione possa provocarne al massimo +5/+6
* per casi peggiori.
*
* @param license string Wikidata item
* @param i int|nil Livello di 
* @see Modulo:Software/man#Considerazioni implementative
* @return UNKNOWN|FREE|PROPRIETARY
]]
local _licenseType_ = {} --cache
function p._licenseType(license, i)
	i = i or 0

	if _licenseType_[license] == nil then

		-- È una licenza radice?
		local type = p._singleLicenseType(license)

		local license_instances = {}
		if type == UNKNOWN then
			-- In `istance of` c'è una licenza radice?
			license_instances = rawProperties(license, 'P31')
			if license_instances then
				type = p._findLicenseType(license_instances)
			end
		end

		local license_classes = {}
		if type == UNKNOWN then
			-- In `subclass of` c'è una licenza radice?
			license_classes = rawProperties(license, 'P279')
			if license_classes then
				type = p._findLicenseType(license_classes)
			end
		end

		if i <= MAX_RECURSION then

			-- Risali `istance of`
			if type == UNKNOWN then
				if license_instances then
					for _, license_instance in pairs(license_instances) do
						type = p._licenseType(license_instance, i + 1)
						if type ~= UNKNOWN then
							break
						end
					end
				end
			end

			-- Risali `subclass of`
			if type == UNKNOWN then
				if type == UNKNOWN and license_classes then
					for _, license_class in pairs(license_classes) do
						type = p._licenseType(license_class, i + 1)
						if type ~= UNKNOWN then
							break
						end
					end
				end
			end

			-- Non si ha altro su cui aggrapparsi

		end

		_licenseType_[license] = type
	end

	return _licenseType_[license]
end

--[[
* Licenze da Wikidata.
*
* @param from string|nil Wikidata item
* @return table|nil
]]
local _licenses_cache = false
function p._licenses(from)
	if _licenses_cache == false then
		_licenses_cache = mWikidata._getClaims('P275', {from = from}) or {}
	end
	return _licenses_cache
end

--[[
* Ha un certo tipo di licenza?
*
* Per motivi di performance dal 29 luglio 2017 si cerca solo fino alla prima licenza identificabile.
*
* @param type FREE|PROPRIETARY|UNKNOWN
* @param from string|nil Wikidata item
* @return true|false
]]
function p._hasALicenseOfType(type, from)
	for _, l in pairs( p._licenses(from) ) do
		l = rawProperty(l)
		local retrievedType = p._licenseType(l)
		if type ~= UNKNOWN then
			return type == retrievedType
		end
	end
	return false
end

--[[
* La voce è sotto licenza di software libero in Wikidata?
*
* @param from string|nil Wikidata item
* @return true|false
]]
function p._hasAFreeLicense(from)
	return p._hasALicenseOfType(FREE, from)
end

--[[
* La voce è sotto licenza di software proprietario in Wikidata?
*
* @param from string|nil Wikidata item
* @return true|false
]]
function p._hasAProprietaryLicense(from)
	return p._hasALicenseOfType(PROPRIETARY, from)
end

--[[
* Categorie legate alle licenze da Wikidata.
*
* @param from string|nil Wikidata Item
* @return string
]]
function p._licenseCategories(from)
	local s = ''
	for i, l in pairs( p._licenses( from ) ) do
		local id = rawProperty(l)
		local name = conf.licenseCategory[id]
		if name then
			local cat = conf.licenseCategoryShorter[id] and LICENSE_CAT_SHORTER or LICENSE_CAT
			s = s .. formatCategory(cat, name) 
		else
			s = s .. UNKNOWN_WD_LICENSE
		end
	end
	return s
end

--[[
* Categorie legate ai linguaggi da Wikidata.
*
* @param from string|nil Wikidata Item
* @return string
]]
function p._languageCategories(from)
	local s = ''
	local languages = mWikidata._getClaims('P277', {from = from} ) -- Property:programming language
	local free      = p._isFreeSoftwareByWikidata(from)
	local missing   = false
	if languages then
		for _, l in pairs( languages ) do
			l = conf.language[ rawProperty(l) ]
			if l then
				s = s .. p._languageCategory(l, free)
			else
				missing = true
			end
		end
	else
		s = noLanguageWarning(free)
	end
	if missing then
		s = s .. UNKNOWN_WD_LANGUAGE
	end
	return s
end

--[[
* Categorie legate ai  toolkit o framework dell'interfaccia grafica da Wikidata.
*
* @param from string|nil Wikidata Item
* @return string
]]
function p._toolkitCategories(from)
	local s = ''
	local toolkits = mWikidata._getClaims('P1414', {from = from} ) -- Property:GUI toolkit or framework
	local free     = p._isFreeSoftwareByWikidata(from)
	local missing  = false
	if toolkits then
		for _, t in pairs( toolkits ) do
			t = conf.language[ rawProperty(t) ]
			if t then
				s = s .. p._languageCategory(t, free)
			else
				missing = true
			end
		end
	end
	if missing then
		s = s .. UNKNOWN_WD_TOOLKIT
	end
	return s
end

--[[
* La categoria di uno specifico linguaggio di programmazione.
* In realtà questa categorizzazione è analoga anche per i toolkit.
*
* @param language table
* @param free boolean|nil È software libero?
* @param man boolean|nil È a fini di documentazione?
* @param deefault string|nil Valore di default nel caso mancasse
* @see Modulo:Software/Configurazione
* @return string
]]
function p._languageCategory(lang, free, man, default)
	local s
	if lang.cat then
		s = free          and lang.free    and "Software libero in %s"       --free = true
		 or free == false and lang.nonfree and "Software proprietario in %s" --free = false
		 or                                    "Software in %s"              --free = nil
		s = formatCategory(s, lang.cat, man)
	end
	return s or default or ''
end

--[[
* Categoria legata alla data di fondazione.
* @todo Capire quale deve prevalere fra data di pubblicazione e data di creazione
* @param from string|nil Wikidata Item
* @param value string|nil Local value
* @return string
]]
function p._yearsCategories(from, value)
	local mCategoryByYear = require('Modulo:Categoria per anno')._main
	local creation = mCategoryByYear( { YEAR_CAT, from = from, value = value, raw = true } )
	if '' == creation then
		return mCategoryByYear( { YEAR_CAT, from = from, value = value, prop = 'P577', checkCat = 'Data di pubblicazione', checkGenre = 'fs' } )
	end
	return mCategoryByYear( { YEAR_CAT, from = from, value = value } )
end

return p