-- DiffAPIDesc.lua

-- Creates a diff file containing documentation that is available from ToLua++'s doxycomment parsing, but not yet included in APIDesc.lua

require("lfs")





--- Translation for function names whose representation in APIDesc is different from the one in Docs
-- Dictionary of "DocsName" -> "DescName"
local g_FunctionNameDocsToDesc =
{
	["new"]    = "constructor",
	["delete"] = "destructor",
	[".add"]   = "operator_plus",
	[".div"]   = "operator_div",
	[".eq"]    = "operator_eq",
	[".mul"]   = "operator_mul",
	[".sub"]   = "operator_sub",
}





--- Translation from C types to Lua types
-- Dictionary of "CType" -> "LuaType"
local g_CTypeToLuaType =
{
	AString = "string",
	bool = "boolean",
	Byte = "number",
	char = "number",
	double = "number",
	float = "number",
	ForEachChunkProvider = "cWorld",
	int = "number",
	size_t = "number",
	unsigned = "number",
	["const AString"] = "string",
	["const char*"] = "string",
	["std::string"] = "string",
	["Vector3<int>"]    = "Vector3i",
	["Vector3<float>"]  = "Vector3f",
	["Vector3<double>"] = "Vector3d",
}





--- Functions that should be ignored
-- Dictionary of "FunctionName" -> true for each ignored function
local g_IgnoreFunction =
{
	destructor = true,
}





local function caseInsensitiveCompare(a_Text1, a_Text2)
	return (a_Text1:lower() < a_Text2:lower())
end





--- Loads the APIDesc.lua and its child files, returns the complete description
-- Returns a table with Classes and Hooks members, Classes being a dictionary of "ClassName" -> { desc }
local function loadAPIDesc()
	-- Load the main APIDesc.lua file:
	local apiDescPath = "../../Server/Plugins/APIDump/"
	local desc = dofile(apiDescPath .. "APIDesc.lua")
	if not(desc) then
		error("Failed to load APIDesc")
	end
	
	-- Merge in the information from all files in the Classes subfolder:
	local classesPath = apiDescPath .. "Classes/"
	for fnam in lfs.dir(apiDescPath .. "Classes") do
		if (string.find(fnam, ".*%.lua$")) then
			local tbls = dofile(classesPath .. fnam)
			for k, cls in pairs(tbls) do
				desc.Classes[k] = cls;
			end
		end
	end
	return desc
end





--- Loads the API documentation generated by ToLua++'s parser
-- Returns a dictionary of "ClassName" -> { docs }
local function loadAPIDocs()
	-- Get the filelist:
	local files = dofile("docs/_files.lua")
	if not(files) then
		error("Failed to load _files.lua from docs")
	end
	
	-- Load the docs from all files, merge into a single dictionary:
	local res = {}
	for _, fnam in ipairs(files) do
		local docs = dofile("docs/" .. fnam)
		if (docs) then
			for k, v in pairs(docs) do
				assert(not(res[k]))  -- Do we have a duplicate documentation entry?
				res[k] = v
			end
		end
	end
	return res
end





--- Returns whether the function signature in the description matches the function documentation
-- a_FunctionDesc is a single description for a function, as loaded from APIDesc.lua (one <FnDesc> item)
-- a_FunctionDoc is a single documentation item for a function, as loaded from ToLua++'s parser
local function functionDescMatchesDocs(a_FunctionDesc, a_FunctionDoc)
	-- Check the number of parameters:
	local numParams = 0
	local numOptionalParams = 0
	if (not(a_FunctionDesc.Params) or (a_FunctionDesc.Params == "")) then
		numParams = 0
	else
		for _, Param in pairs(a_FunctionDesc.Params) do
			numParams = numParams + 1
			if Param.IsOptional then
				numOptionalParams = numOptionalParams + 1
			end
		end
	end
	local numDocParams = #(a_FunctionDoc.Params)
	if ((numDocParams > numParams) or (numDocParams < numParams - numOptionalParams)) then
		return false
	end
	
	return true
end





--- Returns an array of function descriptions that are in a_FunctionDocs but are missing from a_FunctionDescs
-- a_FunctionDescs is an array of function descriptions, as loaded from APIDesc.lua (normalized into array)
-- a_FunctionDocs is an array of function documentation items, as loaded from ToLua++'s parser
-- If all descriptions match, nil is returned instead
local function listMissingClassSingleFunctionDescs(a_FunctionDescs, a_FunctionDocs)
	-- For each documentation item, try to find a match in a_FunctionDescs:
	local res = {}
	for _, docs in ipairs(a_FunctionDocs) do
		local hasFound = false
		for _, desc in ipairs(a_FunctionDescs) do
			if (functionDescMatchesDocs(desc, docs)) then
				hasFound = true
				break
			end
		end  -- for idx - freeDescs[]
		if not(hasFound) then
			table.insert(res, docs)
		end
	end  -- for docs - a_FunctionDocs[]
	
	-- If no result, return nil instead of an empty table:
	if not(res[1]) then
		return nil
	end
	return res
end





--- Returns a dict of "FnName" -> { { <FnDesc> }, ... } that are documented in a_FunctionDocs but missing from a_FunctionDescs
-- If there are no such descriptions, returns nil instead
-- a_FunctionDescs is a dict of "FnName" -> { <FnDescs> } loaded from APIDesc.lua et al
--    <FnDescs> may be a single desc or an array of those
-- a_FunctionDocs is a dict og "FnName" -> { { <FnDesc> }, ... } loaded from ToLua++'s parser
local function listMissingClassFunctionDescs(a_FunctionDescs, a_FunctionDocs)
	-- Match the docs and descriptions for each separate function:
	local res = {}
	local hasSome = false
	a_FunctionDescs = a_FunctionDescs or {}
	a_FunctionDocs = a_FunctionDocs or {}
	for fnName, fnDocs in pairs(a_FunctionDocs) do
		local fnDescName = g_FunctionNameDocsToDesc[fnName] or fnName
		if not(g_IgnoreFunction[fnDescName]) then
			local fnDescs = a_FunctionDescs[fnDescName]
			if not(fnDescs) then
				-- Function not described at all, insert a dummy empty description for the matching:
				fnDescs = {}
			elseif not(fnDescs[1]) then
				-- Function has a single description, convert it to the same format as multi-overload functions use:
				fnDescs = { fnDescs }
			end
			local missingDocs = listMissingClassSingleFunctionDescs(fnDescs, fnDocs)
			if (missingDocs) then
				res[fnName] = missingDocs
				hasSome = true
			end
		end  -- not ignored
	end  -- for fnName, fnDocs - a_FunctionDocs[]
	if not(hasSome) then
		return nil
	end
	return res
end





--- Returns a dictionary of "SymbolName" -> { <desc> } for any variable or constant that is documented but not described
-- a_VarConstDescs is an array of variable or constant descriptions, as loaded from APIDesc.lua
-- a_VarConstDocs is an array of variable or constant documentation items, as loaded from ToLua++'s parser
-- If no symbol is to be returned, returns nil instead
local function listMissingClassVarConstDescs(a_VarConstDescs, a_VarConstDocs)
	-- Match the docs and descriptions for each separate function:
	local res = {}
	local hasSome = false
	a_VarConstDescs = a_VarConstDescs or {}
	a_VarConstDocs = a_VarConstDocs or {}
	for symName, symDocs in pairs(a_VarConstDocs) do
		local symDesc = a_VarConstDescs[symName]
		if (
			not(symDesc) or        -- Symbol not described at all
			not(symDesc.Notes) or  -- Non-existent description
			(
				(symDesc.Notes == "")  and             -- Empty description
				(type(symDocs.Notes) == "string") and  -- Docs has a string ...
				(symDocs.Notes ~= "")                  --  ... that is not empty
			)
		) then
			res[symName] = symDocs
			hasSome = true
		end
	end
	if not(hasSome) then
		return nil
	end
	return res
end





--- Fills a_Missing with descriptions that are documented in a_ClassDocs but missing from a_ClassDesc
-- a_ClassDesc is the class' description loaded from APIDesc et al
-- a_ClassDocs is the class' documentation loaded from ToLua++'s parser
local function listMissingClassDescs(a_ClassName, a_ClassDesc, a_ClassDocs, a_Missing)
	local missing =
	{
		Functions = listMissingClassFunctionDescs(a_ClassDesc.Functions, a_ClassDocs.Functions),
		Constants = listMissingClassVarConstDescs(a_ClassDesc.Constants, a_ClassDocs.Constants),
		Variables = listMissingClassVarConstDescs(a_ClassDesc.Variables, a_ClassDocs.Variables),
	}
	if (
		not(missing.Functions) and
		not(missing.Constants) and
		not(missing.Variables)
	) then
		-- Nothing missing, don't add anything
		return
	end
	a_Missing[a_ClassName] = missing
end





--- Returns a dictionary of "ClassName" -> { { <desc> }, ... } of descriptions that are documented in a_Docs but missing from a_Descs
-- a_Descs is the descriptions loaded from APIDesc et al
-- a_Docs is the documentation loaded from ToLua++'s parser
local function findMissingDescs(a_Descs, a_Docs)
	local descClasses = a_Descs.Classes
	local res = {}
	for clsName, clsDocs in pairs(a_Docs) do
		local clsDesc = descClasses[clsName] or {}
		listMissingClassDescs(clsName, clsDesc, clsDocs, res)
	end
	return res
end





local function outputTable(a_File, a_Table, a_Indent)
	-- Extract all indices first:
	local allIndices = {}
	for k, _ in pairs(a_Table) do
		table.insert(allIndices, k)
	end
	
	-- Sort the indices:
	table.sort(allIndices,
		function (a_Index1, a_Index2)
			if (type(a_Index1) == "number") then
				if (type(a_Index2) == "number") then
					-- Both indices are numeric, sort by value
					return (a_Index1 < a_Index2)
				end
				-- a_Index2 is non-numeric, always goes after a_Index1
				return true
			end
			if (type(a_Index2) == "number") then
				-- a_Index2 is numeric, a_Index1 is not
				return false
			end
			-- Neither index is numeric, use regular string comparison:
			return caseInsensitiveCompare(tostring(a_Index1), tostring(a_Index2))
		end
	)
	
	-- Output by using the index order:
	a_File:write(a_Indent, "{\n")
	local indent = a_Indent .. "\t"
	for _, index in ipairs(allIndices) do
		-- Write the index:
		a_File:write(indent, "[")
		if (type(index) == "string") then
			a_File:write(string.format("%q", index))
		else
			a_File:write(index)
		end
		a_File:write("] =")
		
		-- Write the value:
		local v = a_Table[index]
		if (type(v) == "table") then
			a_File:write("\n")
			outputTable(a_File, v, indent)
		elseif (type(v) == "string") then
			a_File:write(string.format(" %q", v))
		else
			a_File:write(" ", tostring(v))
		end
		a_File:write(",\n")
	end
	a_File:write(a_Indent, "}")
end





--- Returns a description of function params, as used for output
-- a_Params is nil or an array of parameters from ToLua++'s parser
-- a_ClassMap is a dictionary of "ClassName" -> true for all known classes
local function extractParamsForOutput(a_Params, a_ClassMap)
	if not(a_Params) then
		return ""
	end
	assert(a_ClassMap)
	
	local params = {}
	for _, param in ipairs(a_Params) do
		local paramType = param.Type or ""
		paramType = g_CTypeToLuaType[paramType] or paramType  -- Translate from C type to Lua type
		local paramName = param.Name or paramType or "[unknown]"
		paramName = paramName:gsub("^a_", "")  -- Remove the "a_" prefix, if present
		local idxColon = paramType:find("::")  -- Extract children classes and enums within classes
		local paramTypeAnchor = ""
		if (idxColon) then
			paramTypeAnchor = "#" .. paramType:sub(idxColon + 2)
			paramType = paramType:sub(1, idxColon - 1)
		end
		if (a_ClassMap[paramType]) then
			-- Param type is a class name, make it a link
			if not(param.Name) then
				paramName = "{{" .. paramType .. paramTypeAnchor .. "}}"
			else
				paramName = "{{" .. paramType .. paramTypeAnchor .. "|" .. paramName .. "}}"
			end
		end
		table.insert(params, paramName)
	end
	return table.concat(params, ", ")
end





--- Returns a single line of function description for output
-- a_Desc is the function description
-- a_ClassMap is a dictionary of "ClassName" -> true for all known classes
local function formatFunctionDesc(a_Docs, a_ClassMap)
	local staticClause = ""
	if (a_Docs.IsStatic) then
		staticClause = "IsStatic = true, "
	end
	return string.format("{ Params = %q, Return = %q, %sNotes = %q },\n",
		extractParamsForOutput(a_Docs.Params, a_ClassMap),
		extractParamsForOutput(a_Docs.Returns, a_ClassMap),
		staticClause,
		(a_Docs.Desc or ""):gsub("%.\n", ". "):gsub("\n", ". "):gsub("%s+", " ")
	)
end





--- Outputs differences in function descriptions into a file
-- a_File is the output file
-- a_Functions is nil or a dictionary of "FunctionName" -> { { <desc> }, ... }
-- a_ClassMap is a dictionary of "ClassName" -> true for all known classes
local function outputFunctions(a_File, a_Functions, a_ClassMap)
	assert(a_File)
	if not(a_Functions) then
		return
	end
	
	-- Get a sorted array of all function names:
	local fnNames = {}
	for fnName, _ in pairs(a_Functions) do
		table.insert(fnNames, fnName)
	end
	table.sort(fnNames, caseInsensitiveCompare)
	
	-- Output the function descs:
	a_File:write("\t\tFunctions =\n\t\t{\n")
	for _, fnName in ipairs(fnNames) do
		a_File:write("\t\t\t", g_FunctionNameDocsToDesc[fnName] or fnName, " =")
		local docs = a_Functions[fnName]
		if (docs[2]) then
			-- There are at least two descriptions, use the array format:
			a_File:write("\n\t\t\t{\n")
			for _, doc in ipairs(docs) do
				a_File:write("\t\t\t\t", formatFunctionDesc(doc, a_ClassMap))
			end
			a_File:write("\t\t\t},\n")
		else
			-- There's only one description, use the simpler one-line format:
			a_File:write(" ", formatFunctionDesc(docs[1], a_ClassMap))
		end
	end
	a_File:write("\t\t},\n")
end





--- Returns the description of a single variable or constant
-- a_Docs is the ToLua++'s documentation of the symbol
-- a_ClassMap is a dictionary of "ClassName" -> true for all known classes
local function formatVarConstDesc(a_Docs, a_ClassMap)
	local descType = ""
	if (a_Docs.Type) then
		local luaType = g_CTypeToLuaType[a_Docs.Type] or a_Docs.Type
		if (a_ClassMap[a_Docs.Type]) then
			descType = string.format("Type = {{%q}}, ", luaType);
		else
			descType = string.format("Type = %q, ", luaType);
		end
	end
	return string.format("{ %sNotes = %q },\n", descType, a_Docs.Desc or "")
end





--- Outputs differences in variables' or constants' descriptions into a file
-- a_File is the output file
-- a_VarConst is nil or a dictionary of "VariableOrConstantName" -> { <desc> }
-- a_Header is a string, either "Variables" or "Constants"
-- a_ClassMap is a dictionary of "ClassName" -> true for all known classes
local function outputVarConst(a_File, a_VarConst, a_Header, a_ClassMap)
	assert(a_File)
	assert(type(a_Header) == "string")
	if not(a_VarConst) then
		return
	end
	
	-- Get a sorted array of all symbol names:
	local symNames = {}
	for symName, _ in pairs(a_VarConst) do
		table.insert(symNames, symName)
	end
	table.sort(symNames, caseInsensitiveCompare)
	
	-- Output the symbol descs:
	a_File:write("\t\t", a_Header, " =\n\t\t{\n")
	for _, symName in ipairs(symNames) do
		local docs = a_VarConst[symName]
		a_File:write("\t\t\t", symName, " = ", formatVarConstDesc(docs, a_ClassMap))
	end
	a_File:write("\t\t},\n")
end





--- Outputs the diff into a file
-- a_Diff is the diff calculated by findMissingDescs()
-- The output file is written as a Lua source file formatted to match APIDesc.lua
local function outputDiff(a_Diff)
	-- Sort the classnames:
	local classNames = {}
	local classMap = {}
	for clsName, _ in pairs(a_Diff) do
		table.insert(classNames, clsName)
		classMap[clsName] = true
	end
	table.sort(classNames, caseInsensitiveCompare)
	
	-- Output each class:
	local f = assert(io.open("APIDiff.lua", "w"))
	-- outputTable(f, diff, "")
	f:write("return\n{\n")
	for _, clsName in ipairs(classNames) do
		f:write("\t", clsName, " =\n\t{\n")
		local desc = a_Diff[clsName]
		outputFunctions(f, desc.Functions, classMap)
		outputVarConst(f, desc.Variables, "Variables", classMap)
		outputVarConst(f, desc.Constants, "Constants", classMap)
		f:write("\t},\n")
	end
	f:write("}\n")
	f:close()
end





local apiDesc = loadAPIDesc()
local apiDocs = loadAPIDocs()
local diff = findMissingDescs(apiDesc, apiDocs)
outputDiff(diff)
print("Diff has been output to file APIDiff.lua.")



