#!/usr/bin/lua

-- InfoDump.lua

--[[
Loads plugins' Info.lua and dumps its g_PluginInfo into various text formats
This is used for generating plugin documentation for the forum and for GitHub's INFO.md files

This script can be used in two ways:
Executing "lua InfoDump.lua" will go through all subfolders and dump each Info.lua file it can find
	Note that this mode of operation requires LuaRocks with LFS installed; instructions are printed
	when the prerequisites are not met.
Executing "lua InfoDump.lua PluginName" will load the Info.lua file from PluginName's folder and dump
only that one plugin's documentation. This mode of operation doesn't require LuaRocks
--]]





-- Check Lua version. We use 5.1-specific construct when loading the plugin info, 5.2 is not compatible!
if (_VERSION ~= "Lua 5.1") then
	print("Unsupported Lua version. This script requires Lua version 5.1, this Lua is version " .. (_VERSION or "<nil>"));
	return;
end





--- Replaces generic formatting with forum-specific formatting
-- Also removes the single line-ends
local function ForumizeString(a_Str)
	assert(type(a_Str) == "string");
	
	-- Remove the indentation, unless in the code tag:
	-- Only one code or /code tag per line is supported!
	local IsInCode = false;
	local function RemoveIndentIfNotInCode(s)
		if (IsInCode) then
			-- we're in code section, check if this line terminates it
			IsInCode = (s:find("{%%/code}") ~= nil);
			return s .. "\n";
		else
			-- we're not in code section, check if this line starts it
			IsInCode = (s:find("{%%code}") ~= nil);
			return s:gsub("^%s*", "") .. "\n";
		end
	end
	a_Str = a_Str:gsub("(.-)\n", RemoveIndentIfNotInCode);
	
	-- Replace multiple line ends with {%p} and single line ends with a space,
	-- so that manual word-wrap in the Info.lua file doesn't wrap in the forum.
	a_Str = a_Str:gsub("\n\n", "{%%p}");
	a_Str = a_Str:gsub("\n", " ");
	
	-- Replace the generic formatting:
	a_Str = a_Str:gsub("{%%p}", "\n\n");
	a_Str = a_Str:gsub("{%%b}", "[b]"):gsub("{%%/b}", "[/b]");
	a_Str = a_Str:gsub("{%%i}", "[i]"):gsub("{%%/i}", "[/i]");
	a_Str = a_Str:gsub("{%%list}", "[list]"):gsub("{%%/list}", "[/list]");
	a_Str = a_Str:gsub("{%%li}", "[*]"):gsub("{%%/li}", "");
	-- TODO: Other formatting
	
	return a_Str;
end





--- Replaces generic formatting with forum-specific formatting
-- Also removes the single line-ends
local function GithubizeString(a_Str)
	assert(type(a_Str) == "string");
	
	-- Remove the indentation, unless in the code tag:
	-- Only one code or /code tag per line is supported!
	local IsInCode = false;
	local function RemoveIndentIfNotInCode(s)
		if (IsInCode) then
			-- we're in code section, check if this line terminates it
			IsInCode = (s:find("{%%/code}") ~= nil);
			return s .. "\n";
		else
			-- we're not in code section, check if this line starts it
			IsInCode = (s:find("{%%code}") ~= nil);
			return s:gsub("^%s*", "") .. "\n";
		end
	end
	a_Str = a_Str:gsub("(.-)\n", RemoveIndentIfNotInCode);
	
	-- Replace multiple line ends with {%p} and single line ends with a space,
	-- so that manual word-wrap in the Info.lua file doesn't wrap in the forum.
	a_Str = a_Str:gsub("\n\n", "{%%p}");
	a_Str = a_Str:gsub("\n", " ");
	
	-- Replace the generic formatting:
	a_Str = a_Str:gsub("{%%p}", "\n\n");
	a_Str = a_Str:gsub("{%%b}", "**"):gsub("{%%/b}", "**");
	a_Str = a_Str:gsub("{%%i}", "*"):gsub("{%%/i}", "*");
	a_Str = a_Str:gsub("{%%list}", ""):gsub("{%%/list}", "");
	a_Str = a_Str:gsub("{%%li}", " - "):gsub("{%%/li}", "");
	-- TODO: Other formatting
	
	return a_Str;
end





--- Builds an array of categories, each containing all the commands belonging to the category,
-- and the category description, if available.
-- Returns the array table, each item has the following format:
-- { Name = "CategoryName", Description = "CategoryDescription", Commands = {{CommandString = "/cmd verb", Info = {...}}, ...}}
local function BuildCategories(a_PluginInfo)
	-- The returned result
	-- This will contain both an array and a dict of the categories, to allow fast search
	local res = {};
	
	-- For each command add a reference to it into all of its categories:
	local function AddCommands(a_CmdPrefix, a_Commands)
		for cmd, info in pairs(a_Commands) do
			local NewCmd =
			{
				CommandString = a_CmdPrefix .. cmd,
				Info = info,
			}
			
			if ((info.HelpString ~= nil) and (info.HelpString ~= "")) then
				-- Add to each specified category:
				local Category = info.Category;
				if (type(Category) == "string") then
					Category = {Category};
				end
				for idx, cat in ipairs(Category or {""}) do
					local CatEntry = res[cat];
					if (CatEntry == nil) then
						-- First time we came across this category, create it:
						local NewCat = {Name = cat, Description = "", Commands = {NewCmd}};
						table.insert(res, NewCat);
						res[cat] = NewCat;
					else
						-- We already have this category, just add the command to its list of commands:
						table.insert(CatEntry.Commands, NewCmd);
					end
				end  -- for idx, cat - Category[]
			end  -- if (HelpString valid)
			
			-- Recurse all subcommands:
			if (info.Subcommands ~= nil) then
				AddCommands(a_CmdPrefix .. cmd .. " ", info.Subcommands);
			end
		end  -- for cmd, info - a_Commands[]
	end  -- AddCommands()
	
	AddCommands("", a_PluginInfo.Commands);
	
	-- Assign descriptions to categories:
	for name, desc in pairs(a_PluginInfo.Categories or {}) do
		local CatEntry = res[name];
		if (CatEntry ~= nil) then
			-- The result has this category, add the description:
			CatEntry.Description = desc.Description;
		end
	end
	
	-- Alpha-sort each category's command list:
	for idx, cat in ipairs(res) do
		table.sort(cat.Commands,
			function (cmd1, cmd2)
				return (string.lower(cmd1.CommandString) < string.lower(cmd2.CommandString));
			end
		);
	end
	
	return res;
end





--- Returns a string specifying the command.
-- If a_Command is a simple string, returns a_Command colorized to blue
-- If a_Command is a table, expects members Name (full command name) and Params (command parameters),
-- colorizes command name blue and params green
local function GetCommandRefForum(a_Command)
	if (type(a_Command) == "string") then
		return "[color=blue]" .. a_Command .. "[/color]";
	end
	return "[color=blue]" .. a_Command.Name .. "[/color] [color=green]" .. (a_Command.Params or "") .. "[/color]";
end





--- Returns a string specifying the command.
-- If a_CommandParams is nil, returns a_CommandName apostrophed
-- If a_CommandParams is a string, apostrophes a_CommandName with a_CommandParams
local function GetCommandRefGithub(a_CommandName, a_CommandParams)
	assert(type(a_CommandName) == "string");
	if (a_CommandParams == nil) then
		return "`" .. a_CommandName .. "`";
	end
	
	assert(type(a_CommandParams) == "table");
	if ((a_CommandParams.Params == nil) or (a_CommandParams.Params == "")) then
		return "`" .. a_CommandName .. "`";
	end

	assert(type(a_CommandParams.Params) == "string");
	return "`" .. a_CommandName .. " " .. a_CommandParams.Params .. "`";
end





--- Writes the specified command detailed help array to the output file, in the forum dump format
local function WriteCommandParameterCombinationsForum(a_CmdString, a_ParameterCombinations, f)
	assert(type(a_CmdString) == "string");
	assert(type(a_ParameterCombinations) == "table");
	assert(f ~= nil);
	
	if (#a_ParameterCombinations == 0) then
		-- No explicit parameter combinations to write
		return;
	end
	
	f:write("The following parameter combinations are recognized:\n");
	for idx, combination in ipairs(a_ParameterCombinations) do
		f:write("[color=blue]", a_CmdString, "[/color] [color=green]", combination.Params or "", "[/color]");
		if (combination.Help ~= nil) then
			f:write(" - ", ForumizeString(combination.Help));
		end
		if (combination.Permission ~= nil) then
			f:write(" (Requires permission '[color=red]", combination.Permission, "[/color]')");
		end
		f:write("\n");
	end
end





--- Writes the specified command detailed help array to the output file, in the forum dump format
local function WriteCommandParameterCombinationsGithub(a_CmdString, a_ParameterCombinations, f)
	assert(type(a_CmdString) == "string");
	assert(type(a_ParameterCombinations) == "table");
	assert(f ~= nil);
	
	if (#a_ParameterCombinations == 0) then
		-- No explicit parameter combinations to write
		return;
	end
	
	f:write("The following parameter combinations are recognized:\n\n");
	for idx, combination in ipairs(a_ParameterCombinations) do
		f:write(GetCommandRefGithub(a_CmdString, combination));
		if (combination.Help ~= nil) then
			f:write(" - ", GithubizeString(combination.Help));
		end
		if (combination.Permission ~= nil) then
			f:write("   (Requires permission '**", combination.Permission, "**')");
		end
		f:write("\n");
	end
end





--- Writes all commands in the specified category to the output file, in the forum dump format
local function WriteCommandsCategoryForum(a_Category, f)
	-- Write category name:
	local CategoryName = a_Category.Name;
	if (CategoryName == "") then
		CategoryName = "General";
	end
	f:write("\n[size=Large]", ForumizeString(a_Category.DisplayName or CategoryName), "[/size]\n");
	
	-- Write description:
	if (a_Category.Description ~= "") then
		f:write(ForumizeString(a_Category.Description), "\n");
	end
	
	-- Write commands:
	f:write("\n[list]");
	for idx2, cmd in ipairs(a_Category.Commands) do
		f:write("\n[b]", cmd.CommandString, "[/b] - ", ForumizeString(cmd.Info.HelpString or "UNDOCUMENTED"), "\n");
		if (cmd.Info.Permission ~= nil) then
			f:write("Permission required: [color=red]", cmd.Info.Permission, "[/color]\n");
		end
		if (cmd.Info.DetailedDescription ~= nil) then
			f:write(ForumizeString(cmd.Info.DetailedDescription));
		end
		if (cmd.Info.ParameterCombinations ~= nil) then
			WriteCommandParameterCombinationsForum(cmd.CommandString, cmd.Info.ParameterCombinations, f);
		end
	end
	f:write("[/list]\n\n")
end





--- Writes all commands in the specified category to the output file, in the Github dump format
local function WriteCommandsCategoryGithub(a_Category, f)
	-- Write category name:
	local CategoryName = a_Category.Name;
	if (CategoryName == "") then
		CategoryName = "General";
	end
	f:write("\n### ", GithubizeString(a_Category.DisplayName or CategoryName), "\n");
	
	-- Write description:
	if (a_Category.Description ~= "") then
		f:write(GithubizeString(a_Category.Description), "\n\n");
	end
	
	f:write("| Command | Permission | Description | \n")
	f:write("| ------- | ---------- | ----------- | \n")
	
	-- Write commands:
	for idx2, cmd in ipairs(a_Category.Commands) do
		f:write("|", cmd.CommandString, " | ", cmd.Info.Permission or "", " | ", GithubizeString(cmd.Info.HelpString or "UNDOCUMENTED"), "| \n")
	end
	f:write("\n\n")
end





local function DumpCommandsForum(a_PluginInfo, f)
	-- Copy all Categories from a dictionary into an array:
	local Categories = BuildCategories(a_PluginInfo);
	
	-- Sort the categories by name:
	table.sort(Categories,
		function(cat1, cat2)
			return (string.lower(cat1.Name) < string.lower(cat2.Name));
		end
	);
	
	if (#Categories == 0) then
		return;
	end
	
	f:write("\n[size=X-Large]Commands[/size]\n");

	-- Dump per-category commands:
	for idx, cat in ipairs(Categories) do
		WriteCommandsCategoryForum(cat, f);
	end
end





local function DumpCommandsGithub(a_PluginInfo, f)
	-- Copy all Categories from a dictionary into an array:
	local Categories = BuildCategories(a_PluginInfo);
	
	-- Sort the categories by name:
	table.sort(Categories,
		function(cat1, cat2)
			return (string.lower(cat1.Name) < string.lower(cat2.Name));
		end
	);
	
	if (#Categories == 0) then
		return;
	end
	
	f:write("\n# Commands\n");

	-- Dump per-category commands:
	for idx, cat in ipairs(Categories) do
		WriteCommandsCategoryGithub(cat, f);
	end
end





local function DumpAdditionalInfoForum(a_PluginInfo, f)
	local AInfo = a_PluginInfo.AdditionalInfo;
	if (type(AInfo) ~= "table") then
		-- There is no AdditionalInfo in a_PluginInfo
		return;
	end
	
	for idx, info in ipairs(a_PluginInfo.AdditionalInfo) do
		if ((info.Title ~= nil) and (info.Contents ~= nil)) then
			f:write("\n[size=X-Large]", ForumizeString(info.Title), "[/size]\n");
			f:write(ForumizeString(info.Contents), "\n");
		end
	end
end





local function DumpAdditionalInfoGithub(a_PluginInfo, f)
	local AInfo = a_PluginInfo.AdditionalInfo;
	if (type(AInfo) ~= "table") then
		-- There is no AdditionalInfo in a_PluginInfo
		return;
	end
	
	for idx, info in ipairs(a_PluginInfo.AdditionalInfo) do
		if ((info.Title ~= nil) and (info.Contents ~= nil)) then
			f:write("\n# ", GithubizeString(info.Title), "\n");
			f:write(GithubizeString(info.Contents), "\n");
		end
	end
end





--- Collects all permissions mentioned in the info, returns them as a sorted array
-- Each array item is {Name = "PermissionName", Info = { PermissionInfo }}
local function BuildPermissions(a_PluginInfo)
	-- Collect all used permissions from Commands, reference the commands that use the permission:
	local Permissions = a_PluginInfo.Permissions or {};
	local function CollectPermissions(a_CmdPrefix, a_Commands)
		for cmd, info in pairs(a_Commands) do
			CommandString = a_CmdPrefix .. cmd;
			if ((info.Permission ~= nil) and (info.Permission ~= "")) then
				-- Add the permission to the list of permissions:
				local Permission = Permissions[info.Permission] or {};
				Permissions[info.Permission] = Permission;
				-- Add the command to the list of commands using this permission:
				Permission.CommandsAffected = Permission.CommandsAffected or {};
				table.insert(Permission.CommandsAffected, CommandString);
			end
			
			-- Process the command param combinations for permissions:
			local ParamCombinations = info.ParameterCombinations or {};
			for idx, comb in ipairs(ParamCombinations) do
				if ((comb.Permission ~= nil) and (comb.Permission ~= "")) then
					-- Add the permission to the list of permissions:
					local Permission = Permissions[comb.Permission] or {};
					Permissions[info.Permission] = Permission;
					-- Add the command to the list of commands using this permission:
					Permission.CommandsAffected = Permission.CommandsAffected or {};
					table.insert(Permission.CommandsAffected, {Name = CommandString, Params = comb.Params});
				end
			end
			
			-- Process subcommands:
			if (info.Subcommands ~= nil) then
				CollectPermissions(CommandString .. " ", info.Subcommands);
			end
		end
	end
	CollectPermissions("", a_PluginInfo.Commands);
	
	-- Copy the list of permissions to an array:
	local PermArray = {};
	for name, perm in pairs(Permissions) do
		table.insert(PermArray, {Name = name, Info = perm});
	end
	
	-- Sort the permissions array:
	table.sort(PermArray,
		function(p1, p2)
			return (p1.Name < p2.Name);
		end
	);
	return PermArray;
end





local function DumpPermissionsForum(a_PluginInfo, f)
	-- Get the processed sorted array of permissions:
	local Permissions = BuildPermissions(a_PluginInfo);
	if ((Permissions == nil) or (#Permissions <= 0)) then
		return;
	end

	-- Dump the permissions:
	f:write("\n[size=X-Large]Permissions[/size]\n[list]\n");
	for idx, perm in ipairs(Permissions) do
		f:write("  - [color=red]", perm.Name, "[/color] - ");
		f:write(ForumizeString(perm.Info.Description or ""));
		local CommandsAffected = perm.Info.CommandsAffected or {};
		if (#CommandsAffected > 0) then
			f:write("\n[list] Commands affected:\n- ");
			local Affects = {};
			for idx2, cmd in ipairs(CommandsAffected) do
				table.insert(Affects, GetCommandRefForum(cmd));
			end
			f:write(table.concat(Affects, "\n - "));
			f:write("\n[/list]");
		end
		if (perm.Info.RecommendedGroups ~= nil) then
			f:write("\n[list] Recommended groups: ", perm.Info.RecommendedGroups, "[/list]");
		end
		f:write("\n");
	end
	f:write("[/list]");
end





local function DumpPermissionsGithub(a_PluginInfo, f)
	-- Get the processed sorted array of permissions:
	local Permissions = BuildPermissions(a_PluginInfo);
	if ((Permissions == nil) or (#Permissions <= 0)) then
		return;
	end

	-- Dump the permissions:
	f:write("\n# Permissions\n");
	f:write("| Permissions | Description | Commands | Recommended groups |\n")
	f:write("| ----------- | ----------- | -------- | ------------------ |\n")
	for idx, perm in ipairs(Permissions) do
		f:write(perm.Name, " | ");
		f:write(GithubizeString(perm.Info.Description or ""), " | ");
		local CommandsAffected = perm.Info.CommandsAffected or {};
		if (#CommandsAffected > 0) then
			local Affects = {};
			for idx2, cmd in ipairs(CommandsAffected) do
				if (type(cmd) == "string") then
					table.insert(Affects, GetCommandRefGithub(cmd));
				else
					table.insert(Affects, GetCommandRefGithub(cmd.Name, cmd));
				end
			end
			f:write(table.concat(Affects, ", "), " | ");
		end
		if (perm.Info.RecommendedGroups ~= nil) then
			f:write(perm.Info.RecommendedGroups, " |");
		end
		f:write("\n");
	end
end





local function DumpPluginInfoForum(a_PluginFolder, a_PluginInfo)
	-- Open the output file:
	local f, msg = io.open(a_PluginInfo.Name .. "_forum.txt", "w");
	if (f == nil) then
		print("\tCannot dump forum info for plugin " .. a_PluginFolder .. ": " .. msg);
		return;
	end

	-- Write the description:
	f:write(ForumizeString(a_PluginInfo.Description), "\n");
	DumpAdditionalInfoForum(a_PluginInfo, f);
	DumpCommandsForum(a_PluginInfo, f);
	DumpPermissionsForum(a_PluginInfo, f);
	if (a_PluginInfo.SourceLocation ~= nil) then
		f:write("[b][color=blue]Source:[/color] [url=", a_PluginInfo.SourceLocation, "]Link[/url][/b]");
	end
	
	f:close();
end





local function DumpPluginInfoGithub(a_PluginFolder, a_PluginInfo)
	-- Open the output file:
	local f, msg = io.open(a_PluginInfo.Name .. ".md", "w");  -- TODO: Save to a_PluginFolder .. "/Readme.md" instead
	if (f == nil) then
		print("\tCannot dump github info for plugin " .. a_PluginFolder .. ": " .. msg);
		return;
	end

	-- Write the description:
	f:write(GithubizeString(a_PluginInfo.Description), "\n");
	DumpAdditionalInfoGithub(a_PluginInfo, f);
	DumpCommandsGithub(a_PluginInfo, f);
	DumpPermissionsGithub(a_PluginInfo, f);

	f:close();
end





--- Tries to load the g_PluginInfo from the plugin's Info.lua file
-- Returns the g_PluginInfo table on success, or nil and error message on failure
local function LoadPluginInfo(a_FolderName)
	-- Load and compile the Info file:
	local cfg, err = loadfile(a_FolderName .. "/Info.lua");
	if (cfg == nil) then
		return nil, "Cannot open 'Info.lua': " .. err;
	end
	
	-- Execute the loaded file in a sandbox:
	-- This is Lua-5.1-specific and won't work in Lua 5.2!
	local Sandbox = {};
	setfenv(cfg, Sandbox);
	cfg();
	if (Sandbox.g_PluginInfo == nil) then
		return nil, "Info.lua doesn't contain the g_PluginInfo declaration";
	end
	return Sandbox.g_PluginInfo;
end





local function ProcessPluginFolder(a_FolderName)
	local PluginInfo, Msg = LoadPluginInfo(a_FolderName);
	if (PluginInfo == nil) then
		if (Msg ~= nil) then
			print("\t" .. Msg);
		end
		return;
	end
	DumpPluginInfoForum(a_FolderName, PluginInfo);
	DumpPluginInfoGithub(a_FolderName, PluginInfo);
end





--- Tries to load LFS through LuaRocks, returns the LFS instance, or nil on error
local function LoadLFS()
	-- Try to load lfs, do not abort if not found ...
	local lfs, err = pcall(
		function()
			return require("lfs")
		end
	);

	-- ... rather, print a nice message with instructions:
	if not(lfs) then
		print([[
	Cannot load LuaFileSystem
	Install it through luarocks by executing the following command:
		luarocks install luafilesystem (Windows)
		sudo luarocks install luafilesystem (*nix)

	If you don't have luarocks installed, you need to install them using your OS's package manager, usually:
		sudo apt-get install luarocks (Ubuntu / Debian)
	On windows, a binary distribution can be downloaded from the LuaRocks homepage, http://luarocks.org/en/Download
	]]);
		
		print("Original error text: ", err);
		return nil;
	end

	-- We now know that LFS is present, get it normally:
	return require("lfs");
end





local Arg1 = ...;
if ((Arg1 ~= nil) and (Arg1 ~= "")) then
	-- Called with a plugin folder name, export only that one
	ProcessPluginFolder(Arg1)
else
	-- Called without any arguments, process all subfolders:
	local lfs = LoadLFS();
	if (lfs == nil) then
		-- LFS not loaded, error has already been printed, just bail out
		return;
	end
	print("Processing plugin subfolders:");
	for fnam in lfs.dir(".") do
		if ((fnam ~= ".") and (fnam ~= "..")) then
			local Attributes = lfs.attributes(fnam);
			if (Attributes ~= nil) then
				if (Attributes.mode == "directory") then
					print(fnam);
					ProcessPluginFolder(fnam);
				end
			end
		end
	end
end
print("Done.");


