--[[
CAMI - Common Admin Mod Interface.
Makes admin mods intercompatible and provides an abstract privilege interface
for third party addons.

IMPORTANT: This is a draft script. It is very much WIP.

Follows the specification on this page:
https://docs.google.com/document/d/1QIRVcAgZfAYf1aBl_dNV_ewR6P25wze2KmUVzlbFgMI


Structures:
	CAMI_USERGROUP, defines the charactaristics of a usergroup:
	{
		Name
			string
			The name of the usergroup
		Inherits
			string
			The name of the usergroup this usergroup inherits from
	}

	CAMI_PRIVILEGE, defines the charactaristics of a privilege:
	{
		Name
			string
			The name of the privilege
		MinAccess
			string
			One of the following three: user/admin/superadmin
		HasAccess
			function(
				privilege :: CAMI_PRIVILEGE,
				actor     :: Player,
				target    :: Player
			) :: bool
			optional
			Function that decides whether a player can execute this privilege,
			optionally on another player (target).
	}
]]

-- Version number in YearMonthDay format.
local version = 20150902.1

if CAMI and CAMI.Version >= version then return end

CAMI = CAMI or {}
CAMI.Version = version

--[[
usergroups
	Contains the registered CAMI_USERGROUP usergroup structures.
	Indexed by usergroup name.
]]
local usergroups = CAMI.GetUsergroups and CAMI.GetUsergroups() or {
	user = {
		Name = "user",
		Inherits = "user"
	},
	admin = {
		Name = "admin",
		Inherits = "user"
	},
	superadmin = {
		Name = "superadmin",
		Inherits = "admin"
	}
}

--[[
privileges
	Contains the registered CAMI_PRIVILEGE privilege structures.
	Indexed by privilege name.
]]
local privileges = CAMI.GetPrivileges and CAMI.GetPrivileges() or {}

--[[
CAMI.RegisterUsergroup
	Registers a usergroup with CAMI.

	Parameters:
		usergroup
			CAMI_USERGROUP
			(see CAMI_USERGROUP structure)
		source
			any
			Identifier for your own admin mod. Can be anything.
			Use this to make sure CAMI.RegisterUsergroup function and the
			CAMI.OnUsergroupRegistered hook don't cause an infinite loop



	Return value:
		CAMI_USERGROUP
			The usergroup given as argument.
]]
function CAMI.RegisterUsergroup(usergroup, source)
	usergroups[usergroup.Name] = usergroup

	hook.Call("CAMI.OnUsergroupRegistered", nil, usergroup, source)
	return usergroup
end

--[[
CAMI.UnregisterUsergroup
	Unregisters a usergroup from CAMI. This will call a hook that will notify
	all other admin mods of the removal.

	Call only when the usergroup is to be permanently removed.

	Parameters:
		usergroupName
			string
			The name of the usergroup.
		source
			any
			Identifier for your own admin mod. Can be anything.
			Use this to make sure CAMI.UnregisterUsergroup function and the
			CAMI.OnUsergroupUnregistered hook don't cause an infinite loop

	Return value:
		bool
			Whether the unregistering succeeded.
]]
function CAMI.UnregisterUsergroup(usergroupName, source)
	if not usergroups[usergroupName] then return false end

	local usergroup = usergroups[usergroupName]
	usergroups[usergroupName] = nil

	hook.Call("CAMI.OnUsergroupUnregistered", nil, usergroup, source)

	return true
end

--[[
CAMI.GetUsergroups
	Retrieves all registered usergroups.

	Return value:
		Table of CAMI_USERGROUP, indexed by their names.
]]
function CAMI.GetUsergroups()
	return usergroups
end

--[[
CAMI.GetUsergroup
	Receives information about a usergroup.

	Return value:
		CAMI_USERGROUP
			Returns nil when the usergroup does not exist.
]]
function CAMI.GetUsergroup(usergroupName)
	return usergroups[usergroupName]
end

--[[
CAMI.UsergroupInherits
	Returns true when usergroupName1 inherits usergroupName2.
	Note that usergroupName1 does not need to be a direct child.
	Every usergroup trivially inherits itself.

	Parameters:
		usergroupName1
			string
			The name of the usergroup that is queried.
		usergroupName2
			string
			The name of the usergroup of which is queried whether usergroupName1
			inherits from.

	Return value:
		bool
			Whether usergroupName1 inherits usergroupName2.
]]
function CAMI.UsergroupInherits(usergroupName1, usergroupName2)
	repeat
		if usergroupName1 == usergroupName2 then return true end

		usergroupName1 = usergroups[usergroupName1] and
						 usergroups[usergroupName1].Inherits or
						 usergroupName1
	until not usergroups[usergroupName1] or
		  usergroups[usergroupName1].Inherits == usergroupName1

	-- One can only be sure the usergroup inherits from user if the
	-- usergroup isn't registered.
	return usergroupName1 == usergroupName2 or usergroupName2 == "user"
end

--[[
CAMI.InheritanceRoot
	All usergroups must eventually inherit either user, admin or superadmin.
	Regardless of what inheritance mechism an admin may or may not have, this
	always applies.

	This method always returns either user, admin or superadmin, based on what
	usergroups eventually inherit.

	Parameters:
		usergroupName
			string
			The name of the usergroup of which the root of inheritance is
			requested

	Return value:
		string
			The name of the root usergroup (either user, admin or superadmin)
]]
function CAMI.InheritanceRoot(usergroupName)
	if not usergroups[usergroupName] then return end

	local inherits = usergroups[usergroupName].Inherits
	while inherits ~= usergroups[usergroupName].Inherits do
		usergroupName = usergroups[usergroupName].Inherits
	end

	return usergroupName
end

--[[
CAMI.RegisterPrivilege
	Registers a privilege with CAMI.
	Note: do NOT register all your admin mod's privileges with this function!
	This function is for third party addons to register privileges
	with admin mods, not for admin mods sharing the privileges amongst one
	another.

	Parameters:
		privilege
			CAMI_PRIVILEGE
			See CAMI_PRIVILEGE structure.

	Return value:
		CAMI_PRIVILEGE
			The privilege given as argument.
]]
function CAMI.RegisterPrivilege(privilege)
	privileges[privilege.Name] = privilege

	hook.Call("CAMI.OnPrivilegeRegistered", nil, privilege)

	return privilege
end

--[[
CAMI.UnregisterPrivilege
	Unregisters a privilege from CAMI. This will call a hook that will notify
	all other admin mods of the removal.

	Call only when the privilege is to be permanently removed.

	Parameters:
		privilegeName
			string
			The name of the privilege.

	Return value:
		bool
			Whether the unregistering succeeded.
]]
function CAMI.UnregisterPrivilege(privilegeName)
	if not privileges[privilegeName] then return false end

	local privilege = privileges[privilegeName]
	privileges[privilegeName] = nil

	hook.Call("CAMI.OnPrivilegeUnregistered", nil, privilege)

	return true
end

--[[
CAMI.GetPrivileges
	Retrieves all registered privileges.

	Return value:
		Table of CAMI_PRIVILEGE, indexed by their names.
]]
function CAMI.GetPrivileges()
	return privileges
end

--[[
CAMI.GetPrivilege
	Receives information about a privilege.

	Return value:
		CAMI_PRIVILEGE when the privilege exists.
			nil when the privilege does not exist.
]]
function CAMI.GetPrivilege(privilegeName)
	return privileges[privilegeName]
end

--[[
CAMI.PlayerHasAccess
	Queries whether a certain player has the right to perform a certain action.
	Note: this function does NOT return an immediate result!
	The result is in the callback!

	Parameters:
		actorPly
			Player
			The player of which is requested whether they have the privilege.
		privilegeName
			string
			The name of the privilege.
		callback
			function(bool, string)
			This function will be called with the answer. The bool signifies the
			yes or no answer as to whether the player is allowed. The string
			will optionally give a reason.
		targetPly
			Optional.
			The player on which the privilege is executed.
		extraInfoTbl
			Optional.
			Table containing extra information.
			Officially supported members:
				Fallback
					string
					Either of user/admin/superadmin. When no admin mod replies,
					the decision is based on the admin status of the user.
					Defaults to admin if not given.
				IgnoreImmunity
					bool
					Ignore any immunity mechanisms an admin mod might have.
				CommandArguments
					table
					Extra arguments that were given to the privilege command.

	Return value:
		None, the answer is given in the callback function in order to allow
		for the admin mod to perform e.g. a database lookup.
]]
-- Default access handler
local defaultAccessHandler = {["CAMI.PlayerHasAccess"] =
	function(_, actorPly, privilegeName, callback, _, extraInfoTbl)
		-- The server always has access in the fallback
		if not IsValid(actorPly) then return callback(true, "Fallback.") end

		local priv = privileges[privilegeName]

		local fallback = extraInfoTbl and (
			not extraInfoTbl.Fallback and actorPly:IsAdmin() or
			extraInfoTbl.Fallback == "user" and true or
			extraInfoTbl.Fallback == "admin" and actorPly:IsAdmin() or
			extraInfoTbl.Fallback == "superadmin" and actorPly:IsSuperAdmin())


		if not priv then return callback(fallback, "Fallback.") end

		callback(
			priv.MinAccess == "user" or
			priv.MinAccess == "admin" and actorPly:IsAdmin() or
			priv.MinAccess == "superadmin" and actorPly:IsSuperAdmin()
			, "Fallback.")
	end,
	["CAMI.SteamIDHasAccess"] =
	function(_, _, _, callback)
		callback(false, "No information available.")
	end
}
function CAMI.PlayerHasAccess(actorPly, privilegeName, callback, targetPly,
extraInfoTbl)
	hook.Call("CAMI.PlayerHasAccess", defaultAccessHandler, actorPly,
		privilegeName, callback, targetPly, extraInfoTbl)
end

--[[
CAMI.GetPlayersWithAccess
	Finds the list of currently joined players who have the right to perform a
	certain action.
	NOTE: this function will NOT return an immediate result!
	The result is in the callback!

	Parameters:
		privilegeName
			string
			The name of the privilege.
		callback
			function(players)
			This function will be called with the list of players with access.
		targetPly
			Optional.
			The player on which the privilege is executed.
		extraInfoTbl
			Optional.
			Table containing extra information.
			Officially supported members:
				Fallback
					string
					Either of user/admin/superadmin. When no admin mod replies,
					the decision is based on the admin status of the user.
					Defaults to admin if not given.
				IgnoreImmunity
					bool
					Ignore any immunity mechanisms an admin mod might have.
				CommandArguments
					table
					Extra arguments that were given to the privilege command.
]]
function CAMI.GetPlayersWithAccess(privilegeName, callback, targetPly,
extraInfoTbl)
	local allowedPlys = {}
	local allPlys = player.GetAll()
	local countdown = #allPlys

	local function onResult(ply, hasAccess, _)
		countdown = countdown - 1

		if hasAccess then table.insert(allowedPlys, ply) end
		if countdown == 0 then callback(allowedPlys) end
	end

	for _, ply in pairs(allPlys) do
		CAMI.PlayerHasAccess(ply, privilegeName,
			function(...) onResult(ply, ...) end,
			targetPly, extraInfoTbl)
	end
end

--[[
CAMI.SteamIDHasAccess
	Queries whether a player with a steam ID has the right to perform a certain
	action.
	Note: the player does not need to be in the server for this to
	work.

	Note: this function does NOT return an immediate result!
	The result is in the callback!

	Parameters:
		actorSteam
			Player
			The SteamID of the player of which is requested whether they have
			the privilege.
		privilegeName
			string
			The name of the privilege.
		callback
			function(bool, string)
			This function will be called with the answer. The bool signifies the
			yes or no answer as to whether the player is allowed. The string
			will optionally give a reason.
		targetSteam
			Optional.
			The SteamID of the player on which the privilege is executed.
		extraInfoTbl
			Optional.
			Table containing extra information.
			Officially supported members:
				IgnoreImmunity
					bool
					Ignore any immunity mechanisms an admin mod might have.
				CommandArguments
					table
					Extra arguments that were given to the privilege command.

	Return value:
		None, the answer is given in the callback function in order to allow
		for the admin mod to perform e.g. a database lookup.
]]
function CAMI.SteamIDHasAccess(actorSteam, privilegeName, callback,
targetSteam, extraInfoTbl)
	hook.Call("CAMI.SteamIDHasAccess", defaultAccessHandler, actorSteam,
		privilegeName, callback, targetSteam, extraInfoTbl)
end

--[[
CAMI.SignalUserGroupChanged
	Signify that your admin mod has changed the usergroup of a player. This
	function communicates to other admin mods what it thinks the usergroup
	of a player should be.

	Listen to the hook to receive the usergroup changes of other admin mods.

	Parameters:
		ply
			Player
			The player for which the usergroup is changed
		old
			string
			The previous usergroup of the player.
		new
			string
			The new usergroup of the player.
		source
			any
			Identifier for your own admin mod. Can be anything.
]]
function CAMI.SignalUserGroupChanged(ply, old, new, source)
	hook.Call("CAMI.PlayerUsergroupChanged", nil, ply, old, new, source)
end

--[[
CAMI.SignalSteamIDUserGroupChanged
	Signify that your admin mod has changed the usergroup of a disconnected
	player. This communicates to other admin mods what it thinks the usergroup
	of a player should be.

	Listen to the hook to receive the usergroup changes of other admin mods.

	Parameters:
		ply
			string
			The steam ID of the player for which the usergroup is changed
		old
			string
			The previous usergroup of the player.
		new
			string
			The new usergroup of the player.
		source
			any
			Identifier for your own admin mod. Can be anything.
]]
function CAMI.SignalSteamIDUserGroupChanged(steamId, old, new, source)
	hook.Call("CAMI.SteamIDUsergroupChanged", nil, steamId, old, new, source)
end
