-- Credits mod.
-- This mod stores the credits in data/credits.csv and can both generate a
-- CREDITS text for the CREDITS.md Markdown file as well display
-- an in-game dialog.
-- The file format of credits.csv is described in data/README.md

lzr_credits = {}

local S, PS = minetest.get_translator("lzr_credits")
local F = minetest.formspec_escape
local H = minetest.hypertext_escape
local ESC = function(arg)
	return F(arg)
end

-- Set to true to print a list of translatable
-- credits strings to console. Helpful to update
-- the locale files
local PRINT_TRANSLATABLE_CREDITS_STRINGS = minetest.settings:get_bool("lzr_print_special_translatable_strings", false)

-- Print Markdown-formatted text of credits to
-- console on startup.
local PRINT_MARKDOWN = false

-- If true, will check all files if they are credited and
-- prints an error for every uncredited file.
local CHECK_MISSING_CREDITS = false

-- Color of "official" author name
local COLOR_AUTHOR_OFFINAME = "#FFFF00"
-- Color of author nickname
local COLOR_AUTHOR_NICKNAME = "#00FF00"
-- Color of text representing multiple authors
local COLOR_AUTHOR_MULTINAME = "#FF9F00"

-- Whether to show the full credits in the credits formspec
local state_full_credits = false

local function load_credits_file()
	local credits_file = io.open(minetest.get_modpath("lzr_credits").."/data/credits.csv", "r")
	if not credits_file then
		minetest.log("error", "[lzr_credits] Error loading credits.csv")
		return false
	end

	local credits_string = credits_file:read("*a")

	-- Parse credits.csv. See data/README.md for the meaning of the rows.
	local credits, csv_error = lzr_csv.parse_csv(credits_string)
	credits_file:close()
	if not credits then
		minetest.log("error", "[lzr_credits] Error parsing credits.csv file: "..tostring(csv_error))
		return false
	end
	return true, credits
end

local spdx_licenses = {
	-- Standard SPDX licenses <https://spdx.org/licenses/>
	["GPL-3.0-or-later"] = "GPLv3+",
	["GPL-2.0-or-later"] = "GPLv2+",
	["LGPL-2.1-or-later"] = "LGPLv2.1+",
	["MIT"] = "MIT",
	["CC0-1.0"] = "CC0",
	["CC-BY-3.0"] = "CC BY 3.0",
	["CC-BY-4.0"] = "CC BY 4.0",
	["CC-BY-SA-3.0"] = "CC BY 3.0",
	["CC-BY-SA-4.0"] = "CC BY 4.0",
	["BSD-3-Clause"] = "BSD 3-Clause",
	["OFL-1.1"] = "OFL 1.1",

	-- When a license does not apply.
	-- NOTE: Only use this for things where copyright in general
	-- does not apply, e.g. for abstract things like ideas.
	["NONE"] = "",

	-- Licenses not (yet) recognized in SPDX (use identifiers of the form "LicenseRef-n"
	-- where n is a number):

	-- OGA-BY 3.0: OpenGameArt.org Legal Code Attribution 3.0 Unported
	-- <https://static.opengameart.org/OGA-BY-3.0.txt>
	["LicenseRef-1"] = "OGA-BY 3.0",
}

-- List of header names in which the credited_for value for
-- all authors must be displayed in abridged mode.
-- IMPORTANT: The header names MUST match the value in credits.csv!
-- Normally, abridged mode will hide the credited_for value
-- so only a list of authors is shown.
local forced_credited_for_sections = {
	-- To show the languages
	["Translations"] = true,
	-- To show music track names
	["Music"] = true,
}

-- Parse credits.csv and output it as Markdown into the console.
-- The output is intentionally NOT translatable.
local function parse_and_print_markdown()
	local load_status, credits = load_credits_file()
	if not load_status then
		return
	end
	local markdown = ""
	markdown = markdown ..

[[# Credits for Lazarr!

Lazarr! is a pirate-themed laser puzzle game designed by Wuzzy.

This game uses many artworks from other people, mostly from
the Luanti community and independent artwork sharing communities like
freesound.org and opengameart.org. Without these communities,
this game would not be possible.

This game is free software (including the artwork). See README.md
for the licensing of the game as a whole.

A list of all people who worked on this game, both directly and
indirectly, follows:]] .. "\n\n"

	-- Escape a string for Markdown
	local md_escape = function(str)
		str = string.gsub(str, "\\", "\\\\")
		str = string.gsub(str, "_", "\\_")
		str = string.gsub(str, "*", "\\*")
		return str
	end

	for c=1, #credits do
		local row = credits[c]
		local ctype = row[1]

		-- Header
		if ctype == "header" then
			local text = row[2]
			if c > 1 then
				markdown = markdown .. "\n"
			end
			markdown = markdown .. "## "..md_escape(text).."\n\n"

		-- Person or people
		elseif ctype == "person" or ctype == "people" then
			local nickname = md_escape(row[2])
			local offiname = md_escape(row[3])
			local credited_for = md_escape(row[4])
			local url = row[6]
			local orig_file_name = row[7]
			local game_file_name = row[8]
			local license = md_escape(row[9])
			local modified = row[10]

			if nickname ~= "" and offiname ~= "" then
				if credited_for ~= "" then
					markdown = markdown .. "- " ..string.format("%s: %s (alias %s)", credited_for, offiname, nickname)
				else
					markdown = markdown .. "- " ..string.format("%s (alias %s)", offiname, nickname)
				end
			elseif nickname ~= "" or offiname ~= "" then
				local name
				if nickname ~= "" then
					name = nickname
				else
					name = offiname
				end
				if credited_for ~= "" then
					markdown = markdown .. "- " ..string.format("%s: %s", credited_for, name)
				else
					markdown = markdown .. "- " ..name
				end
			end
			markdown = markdown .. "\n"

			-- Split list of file names. File name argument may contain
			-- multiple files, each separated by the pipe character "|".
			-- Also add backticks for correct Markdown formatting.
			local split_files_md = function(str)
				local sfiles = string.split(str, "|")
				if not sfiles then
					return "", 0
				end
				local smfiles = {}
				for s=1, #sfiles do
					table.insert(smfiles, "`"..sfiles[s].."`")
				end
				return table.concat(smfiles, ", "), #smfiles
			end
			if game_file_name and game_file_name ~= "" then
				local sfiles, snum = split_files_md(game_file_name)
				if snum == 1 then
					markdown = markdown .. "	- File name: "..sfiles.."\n"
				else
					markdown = markdown .. "	- File names: "..sfiles.."\n"
				end
			end
			if orig_file_name and orig_file_name ~= "" then
				local sfiles, snum = split_files_md(orig_file_name)
				if snum == 1 then
					markdown = markdown .. "	- Original file name: "..sfiles.."\n"
				else
					markdown = markdown .. "	- Original file names: "..sfiles.."\n"
				end
			end
			if url and url ~= "" then
				markdown = markdown .. "	- Origin: <"..url..">\n"
			end
			if modified == "2" then
				markdown = markdown .. "	- Modifications were made\n"
			end
			if license and license ~= "" then
				if license ~= "NONE" then
					local readable_license = spdx_licenses[license]
					if not readable_license then
						minetest.log("error", "[lzr_credits] Unknown license identifier in line "..c..": "..tostring(license))
						break
					end
					markdown = markdown .. "	- License: "..readable_license.."\n"
				end
			else
				minetest.log("warning", "[lzr_credits] Missing license identifier in line "..c)
			end

		-- Unknown credits type (error)
		else
			minetest.log("error", "[lzr_credits] Unknown credits type in line "..c..": "..tostring(ctype))
			break
		end
	end
	-- Appendix

	markdown = markdown .."\n" ..
[[## Other stuff

- See the licence files of the individual mods]]

.."\n\n"..

[[## File licensing

Every file is (at least) dual-licensed. This means it is both licensed
under the terms of the game license (see README.md) as well as an individual
file license (shown in this list).

Only licenses compatible with free software are used in this game.

## License references

This is a reference of all license name abbreviations of the licenses listed above:

* GPLv3+: GNU General Public License version 3, or any later version <https://www.gnu.org/licenses/gpl-3.0.en.html>
* GPLv2+: GNU General Public License version 2, or any later version <https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html>
* LGPLv2.1+: GNU Lesser General Public License version 2.1, or any later version <https://www.gnu.org/licenses/old-licenses/lgpl-2.1>
* MIT: The MIT License <https://mit-license.org/>
* BSD 3-Clause: The BSD 3-Clause License <https://opensource.org/license/BSD-3-clause>
* CC0: Creative Commons Zero <https://creativecommons.org/publicdomain/zero/1.0/>
* CC BY 3.0: Creative Commons Attribution 3.0 Unported <https://creativecommons.org/licenses/by/3.0/>
* CC BY 4.0: Creative Commons Attribution 4.0 International <https://creativecommons.org/licenses/by/4.0/>
* CC BY-SA 3.0: Creative Commons Attribution-ShareAlike 3.0 Unported <https://creativecommons.org/licenses/by-sa/3.0/>
* CC BY-SA 4.0: Creative Commons Attribution-ShareAlike 4.0 International <https://creativecommons.org/licenses/by-sa/4.0/>
* OGA-BY 3.0: OpenGameArt.org Legal Code Attribution 3.0 Unported <https://static.opengameart.org/OGA-BY-3.0.txt>
* OFL-1.1: SIL Open Font License version 1.1 <https://openfontlicense.org/open-font-license-official-text/>]]

	local output = "---- BEGIN OF CREDITS MARKDOWN TEXT ----\n" ..
		markdown .. "\n" ..
		"---- END OF CREDITS MARKDOWN TEXT ----"
	print(output)
end

-- Parse credits.csv print the translatable strings to the
-- console, from where they can be pasted in to *.lua files.
local function parse_and_print_strings_to_console()
	local load_status, credits = load_credits_file()
	if not load_status then
		return
	end

	print("-- TRANSLATABLE STRINGS FOR CREDITS [lzr_credits] --")

	for c=1, #credits do
		local row = credits[c]
		local ctype = row[1]
		if ctype == "header" then
			local text = row[2]
			print(string.format('NS(%q)', text))
		elseif ctype == "person" or ctype == "people" then
			local credited_for = row[4]
			local credited_for_translatable = row[5] == "1"

			if credited_for and credited_for ~= "" and credited_for_translatable then
				print(string.format('NS(%q)', credited_for))
			end
		end
	end
end

-- Parse credits.csv and show formspec to player.
local function parse_and_show_credits(player)
	local load_status, credits = load_credits_file()
	if not load_status then
		return
	end
	local form = "formspec_version[7]size[12,10.5]"

	-- Hypertext element
	form = form .. "box[0.5,0.5;11,9;#00000080]"
	form = form .. "hypertext[0.5,0.5;11,9;credits;"

	form = form .. "<global margin='7' color='#ffffff'>"
	form = form .. "<tag name='_extra' color='#bbbbbb'>"
	form = form .. "<tag name='action' color='#8888ff' hovercolor='#ffbbbb'>"

	form = form .. "<bigger>"..ESC(S("Credits for Lazarr!")).."</bigger>\n"

	local used_offinames_in_current_section = {}
	local used_nicknames_in_current_section = {}
	local current_section = ""

	local url_action_counter = 0
	for c=1, #credits do
		local row = credits[c]
		local ctype = row[1]

		-- Header
		if ctype == "header" then
			local text = row[2]
			if c > 1 then
				form = form .. "\n"
			end
			form = form .. "<big>"..ESC(S(text)).."</big>\n"
			current_section = text
			used_offinames_in_current_section = {}
			used_nicknames_in_current_section = {}

		-- Person or people
		elseif ctype == "person" or ctype == "people" then
			local nickname = row[2]
			local offiname = row[3]
			local credited_for = row[4]
			local credited_for_translatable = row[5] == "1"

			if credited_for and credited_for ~= "" and credited_for_translatable then
				print(string.format('NS(%q)', credited_for))
			end

			-- In abridged credits, don't list people twice per-section
			local skip_person = false
			if not state_full_credits and not forced_credited_for_sections[current_section] then
				if used_offinames_in_current_section[offiname] or used_nicknames_in_current_section[nickname] then
					skip_person = true
				end
				if offiname ~= "" then
					used_offinames_in_current_section[offiname] = true
				end
				if nickname ~= "" then
					used_nicknames_in_current_section[nickname] = true
				end
			end

			if not skip_person then

				local url = row[6]
				local orig_file_name = row[7]
				local game_file_name = row[8]
				local license = row[9]
				local modified = row[10]

				local credited_for_form
				if (state_full_credits or forced_credited_for_sections[current_section]) and credited_for and credited_for ~= "" then
					if credited_for_translatable then
						credited_for_form = S(credited_for)
					else
						credited_for_form = "<i>"..credited_for.."</i>"
					end
				else
					credited_for_form = ""
				end
				if nickname ~= "" and offiname ~= "" then
					if credited_for_form ~= "" then
						--[[~ Credits line. @1 = what the author is credited for, @2 = official/formal author name, @3 author nickname
						IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
						form = form .. S("@1: @2 (alias @3)",
							credited_for_form,
							"<b><style color='"..COLOR_AUTHOR_OFFINAME.."'>"..ESC(offiname).."</style></b>",
							"<b><style color='"..COLOR_AUTHOR_NICKNAME.."'>"..ESC(nickname).."</style></b>")
					else
						--[[~ Credits line. @1 = official/formal author name, @2 author nickname
						IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
						form = form .. S("@1 (alias @2)",
							"<b><style color='"..COLOR_AUTHOR_OFFINAME.."'>"..ESC(offiname).."</style></b>",
							"<b><style color='"..COLOR_AUTHOR_NICKNAME.."'>"..ESC(nickname).."</style></b>")
					end
				elseif nickname ~= "" or offiname ~= "" then
					local name
					if nickname ~= "" then
						local color
						if ctype == "people" then
							color = COLOR_AUTHOR_MULTINAME
						else
							color = COLOR_AUTHOR_NICKNAME
						end
						name = "<b><style color='"..color.."'>"..ESC(nickname).."</style></b>"
					else
						name = "<b><style color='"..COLOR_AUTHOR_OFFINAME.."'>"..ESC(offiname).."</style></b>"
					end
					if credited_for_form ~= "" then
						--[[~ Credits line. @1 = what the author is credited for, @2 = author name
						IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
						form = form .. S("@1: @2", credited_for_form, ESC(name))
					else
						form = form .. ESC(name)
					end
				else
					minetest.log("error", "[lzr_credits] Missing name in credits, line "..c)
				end

				if state_full_credits then
					form = form .. "\n"
					-- Split list of file names. File name argument may contain
					-- multiple files, each separted by the pipe character "|"
					local split_files = function(str)
						local sfiles = string.split(str, "|")
						if not sfiles then
							return "", 0
						end
						--~ list separator
						return table.concat(sfiles, S(", ")), #sfiles
					end
					if game_file_name and game_file_name ~= "" then
						local sfiles, snum = split_files(game_file_name)
						if snum > 0 then
							--[[~ List of file names.
							IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
							form = form .. "<_extra>"..PS("• File name: @1", "• File names: @1", snum, ESC(sfiles)).."</_extra>\n"
						end
					end
					if orig_file_name and orig_file_name ~= "" then
						local sfiles, snum = split_files(orig_file_name)
						if snum > 0 then
							--[[~ List of original file names.
							IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
							form = form .. "<_extra>"..PS("• Original file name: @1", "• Original file names: @1", snum, ESC(sfiles)).."</_extra>\n"
						end
					end
					if url and url ~= "" then
						url_action_counter = url_action_counter + 1
						--[[~ For the URL of the origin of a credited file. @1 = the URL.
						IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
						form = form .. "<_extra>"..S("• Origin: @1", "<action name='url_"..url_action_counter.."' url='"..url.."'>"..url.."</action>").."</_extra>\n"
					end
					if modified == "2" then
						--[[~ Annotation for modified files
						IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
						form = form .. "<_extra>"..S("• Modifications were made").."</_extra>\n"
					end
					if license and license ~= "" then
						if license ~= "NONE" then
							local readable_license = spdx_licenses[license]
							if not readable_license then
								minetest.log("error", "[lzr_credits] Unknown license identifier in line "..c..": "..tostring(license))
								break
							end
							--[[~ File license.
							IMPORTANT: If a "<", ">", or "\" occurs in the translation, you MUST escape it by putting a backslash in front ]]
							form = form .. "<_extra>"..S("• License: @1", readable_license).."</_extra>\n"
						end
					else
						minetest.log("warning", "[lzr_credits] Missing license identifier in line "..c)
					end
				end
				form = form .. "\n"
			end

		-- Unknown credits type (error)
		else
			minetest.log("error", "[lzr_credits] Unknown credits type in line "..c..": "..tostring(ctype))
			break
		end
	end
	-- End of hypertext element
	form = form .. "]"

	-- To toggle the full credits display state
	form = form .. "checkbox[0.5,9.9;full_credits;"..F(S("Display full credits"))..";"..tostring(state_full_credits).."]"
	form = form .. "tooltip[full_credits;"..F(S("Display additional information, including license, file names and links to the source material"))

	minetest.show_formspec(player:get_player_name(), "lzr_credits:credits", form)
	return true
end

-- Toggle full credits state
minetest.register_on_player_receive_fields(function(player, form, fields)
	if form ~= "lzr_credits:credits" then
		return
	end
	if fields.full_credits == "true" then
		state_full_credits = true
		parse_and_show_credits(player)
	elseif fields.full_credits == "false" then
		state_full_credits = false
		parse_and_show_credits(player)
	end
end)

minetest.register_chatcommand("credits", {
	description = S("Display the list of people who worked on this game"),
	params = "",
	func = function(name, param)
		local player = minetest.get_player_by_name(name)
		if not player then
			return false
		end
		parse_and_show_credits(player)
		return true
	end,
})

lzr_credits.show_credits = function(player)
	parse_and_show_credits(player)
end

if PRINT_MARKDOWN then
	minetest.register_on_mods_loaded(function()
		parse_and_print_markdown()
	end)
end

-- Print translatable strings to console on startup, if enabled
if PRINT_TRANSLATABLE_CREDITS_STRINGS then
	minetest.register_on_mods_loaded(function()
		parse_and_print_strings_to_console()
	end)
end

if CHECK_MISSING_CREDITS then
minetest.register_on_mods_loaded(function()
	local mods = minetest.get_modnames()
	local media_files = {}
	for m=1, #mods do
		local modpath = minetest.get_modpath(mods[m])
		local subdirs = minetest.get_dir_list(modpath, true)
		for s=1, #subdirs do
			local subdir = subdirs[s]
			if subdir == "textures" or subdir == "models" or subdir == "sounds" or subdir == "fonts" then
				local files = minetest.get_dir_list(modpath.."/"..subdirs[s], false)
				for f=1, #files do
					table.insert(media_files, files[f])
				end
			end
		end
	end

	local load_status, credits = load_credits_file()
	if not load_status then
		return
	end
	local credited_files = {}
	for c=1, #credits do
		local row = credits[c]
		local ctype = row[1]
		-- Person or people
		if ctype == "person" or ctype == "people" then
			local game_file_names = row[8]
			local tfiles = string.split(game_file_names, "|")
			if not tfiles then
				tfiles = {}
			end
			for t=1, #tfiles do
				table.insert(credited_files, tfiles[t])
			end
		end
	end

	-- Explicitly add the file names of the menu/ directory of this game
	-- so they count as 'found'
	local menu_files = { "background.png", "footer.png", "header.png", "header.svg", "icon.png" }
	for m=1, #menu_files do
		table.insert(media_files, menu_files[m])
	end

	table.sort(credited_files)
	table.sort(media_files)
	for c=1, #credited_files do
		local cfile = credited_files[c]
		local contains_wildcard = string.match(cfile, "*") ~= nil
		local pattern
		if contains_wildcard then
			pattern = string.gsub(cfile, "%.", "%%.")
			pattern = string.gsub(pattern, "%*", ".*")
		end

		local found = false
		for m=1, #media_files do
			if pattern then
				if string.find(media_files[m], pattern) then
					found = true
					break
				end
			else
				if cfile == media_files[m] then
					found = true
					break
				end
			end
		end
		if not found then
			minetest.log("error", "[lzr_credits] File in credits does not exist: "..cfile)
		end
	end

	for m=1, #media_files do
		local found = false
		for c=1, #credited_files do
			local cfile = credited_files[c]
			local contains_wildcard = string.match(cfile, "*") ~= nil
			local pattern
			if contains_wildcard then
				pattern = string.gsub(cfile, "%.", "%%.")
				pattern = string.gsub(pattern, "%*", ".*")
			end
			if pattern then
				if string.find(media_files[m], pattern) then
					found = true
					break
				end
			else
				if cfile == media_files[m] then
					found = true
					break
				end
			end
		end
		if not found then
			minetest.log("error", "[lzr_credits] File is not credited: "..media_files[m])
		end
	end
end)
end


