-- Global to all starfalls
local checkluatype = SF.CheckLuaType

-- Register privileges
SF.Permissions.registerPrivilege("find", "Find", "Allows the user to access the find library")

--- Find library. Finds entities in various shapes.
-- @name find
-- @class library
-- @libtbl find_library
SF.RegisterLibrary("find")

return function(instance)
	local checkpermission = instance.player ~= SF.Superuser and SF.Permissions.check or function() end

	local find_library = instance.Libraries.find
	local owrap, ounwrap = instance.WrapObject, instance.UnwrapObject
	local vec_meta, vwrap, vunwrap = instance.Types.Vector, instance.Types.Vector.Wrap, instance.Types.Vector.Unwrap
	local plywrap = instance.Types.Player.Wrap

	local vunwrap1, vunwrap2, vunwrap3, vunwrap4
	instance:AddHook("initialize", function()
		vunwrap1, vunwrap2, vunwrap3, vunwrap4 =
			vec_meta.QuickUnwrap1, vec_meta.QuickUnwrap2, vec_meta.QuickUnwrap3, vec_meta.QuickUnwrap4
	end)

	local function convert(results, func)
		local t = {}
		if func ~= nil then
			checkluatype(func, TYPE_FUNCTION, 1)
			for i = 1, #results do
				local e = owrap(results[i])
				if e and func(e) then
					t[#t + 1] = e
				end
			end
		else
			for i = 1, #results do
				local e = owrap(results[i])
				if e then
					t[#t + 1] = e
				end
			end
		end
		return t
	end

	local function convertIter(iter, func)
		local t = {}
		if func ~= nil then
			checkluatype(func, TYPE_FUNCTION, 1)
			for _, v in iter() do
				local e = owrap(v)
				if e and func(e) then
					t[#t + 1] = e
				end
			end
		else
			for _, v in iter() do
				local e = owrap(v)
				if e then
					t[#t + 1] = e
				end
			end
		end
		return t
	end

	--- Finds entities in a box
	-- @param Vector min Bottom corner
	-- @param Vector max Top corner
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.inBox(min, max, filter)
		checkpermission(instance, nil, "find")

		return convert(ents.FindInBox(vunwrap1(min), vunwrap2(max)), filter)
	end

	--- Finds entities in a sphere
	-- @param Vector center Center of the sphere
	-- @param number radius Sphere radius
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.inSphere(center, radius, filter)
		checkpermission(instance, nil, "find")
		checkluatype(radius, TYPE_NUMBER)

		return convert(ents.FindInSphere(vunwrap1(center), radius), filter)
	end

	--- Finds entities in a cone
	-- @param Vector pos The cone vertex position
	-- @param Vector dir The direction to project the cone
	-- @param number distance The length to project the cone
	-- @param number radius The cosine of angle of the cone. 1 makes a 0° cone, 0.707 makes approximately 90°, 0 makes 180°, and so on.
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.inCone(pos, dir, distance, radius, filter)
		checkpermission(instance, nil, "find")
		checkluatype(distance, TYPE_NUMBER)
		checkluatype(radius, TYPE_NUMBER)

		return convert(ents.FindInCone(vunwrap1(pos), vunwrap2(dir), distance, radius), filter)
	end

	--- Finds entities in a ray
	-- @param Vector startpos The ray start
	-- @param Vector endpos The ray end
	-- @param Vector? mins If not nil, will define a lower bound of the ray's hull
	-- @param Vector? maxs If not nil, will define a upper bound of the ray's hull
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.inRay(startpos, endpos, mins, maxs, filter)
		checkpermission(instance, nil, "find")

		startpos = vunwrap1(startpos)
		endpos = vunwrap2(endpos)

		if mins ~= nil or maxs ~= nil then
			mins = vunwrap3(mins)
			maxs = vunwrap4(maxs)
		end

		return convert(ents.FindAlongRay(startpos, endpos, mins, maxs), filter)
	end

	--- Finds entities by class name
	-- @param string class The class name
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.byClass(class, filter)
		checkpermission(instance, nil, "find")
		checkluatype(class, TYPE_STRING)

		return convert(ents.FindByClass(class), filter)
	end

	--- Finds entities by their targetname
	-- @param string name The targetname
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.byName(name, filter)
		checkpermission(instance, nil, "find")
		checkluatype(name, TYPE_STRING)

		return convert(ents.FindByName(name), filter)
	end

	--- Finds entities by model
	-- @param string model The model file
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.byModel(model, filter)
		checkpermission(instance, nil, "find")
		checkluatype(model, TYPE_STRING)

		return convert(ents.FindByModel(model), filter)
	end

	if SERVER then
		--- Finds entities that are in the PVS (Potentially Visible Set). See: https://developer.valvesoftware.com/wiki/PVS
		-- @server
		-- @param Vector pos Vector view point
		-- @param function? filter Optional function to filter results
		-- @return table An array of found entities
		function find_library.inPVS(pos, filter)
			checkpermission(instance, nil, "find")
			return convert(ents.FindInPVS(vunwrap1(pos)), filter)
		end
	end

	--- Finds all players (including bots)
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.allPlayers(filter)
		checkpermission(instance, nil, "find")

		return convertIter(player.Iterator, filter)
	end

	--- Finds all entities
	-- @param function? filter Optional function to filter results
	-- @return table An array of found entities
	function find_library.all(filter)
		checkpermission(instance, nil, "find")

		return convertIter(ents.Iterator, filter)
	end

	--- Finds the closest entity to a point
	-- @param table ents The array of entities
	-- @param Vector pos The position
	-- @return Entity The closest entity
	function find_library.closest(ents, pos)
		local closest = math.huge
		local closestent

		for k, v in pairs(ents) do
			local d = v:getPos():getDistanceSqr(pos)
			if d < closest then
				closest = d
				closestent = v
			end
		end

		return closestent
	end

	--- Returns a sorted array of entities by how close they are to a point
	-- @param table ents The array of entities
	-- @param Vector pos The position
	-- @param boolean furthest Whether to have the further entities first
	-- @return table A table of the closest entities
	function find_library.sortByClosest(ents, pos, furthest)
		local distances = {}
		for i = 1, #ents do
			distances[i] = { ents[i]:getPos():getDistanceSqr(pos), ents[i] }
		end
		local sortfunc
		if furthest then
			sortfunc = function(a, b)
				return a[1] > b[1]
			end
		else
			sortfunc = function(a, b)
				return a[1] < b[1]
			end
		end
		table.sort(distances, sortfunc)
		local ret = {}
		for i = 1, #distances do
			ret[i] = distances[i][2]
		end
		return ret
	end

	--- Finds the first player with the given name
	-- @param string name Name to search for
	-- @param boolean? casesensitive Boolean should the match be case sensitive?
	-- @param boolean? exact Boolean should the name match exactly
	-- @return table Table of found players
	function find_library.playersByName(name, casesensitive, exact)
		checkpermission(instance, nil, "find")
		checkluatype(name, TYPE_STRING)
		if casesensitive ~= nil then
			checkluatype(casesensitive, TYPE_BOOL)
		end
		if exact ~= nil then
			checkluatype(exact, TYPE_BOOL)
		end

		local ret = {}
		local getName
		if casesensitive then
			getName = function(ply)
				return ply:GetName()
			end
		else
			name = string.lower(name)
			getName = function(ply)
				return string.lower(ply:GetName())
			end
		end

		if exact then
			for k, ply in ipairs(player.GetAll()) do
				if getName(ply) == name then
					ret[#ret + 1] = plywrap(ply)
				end
			end
		else
			for k, ply in ipairs(player.GetAll()) do
				if string.find(getName(ply), name, 1, true) then
					ret[#ret + 1] = plywrap(ply)
				end
			end
		end

		return ret
	end

	--- Finds the player with the given a steamid. Returns nil if not found
	-- @param string steamid Steam Id to search for
	-- @return Player? The player with matching steamid
	function find_library.playerBySteamID(steamid)
		local found = player.GetBySteamID(steamid)
		if found then
			return plywrap(found)
		end
	end

	--- Finds the player with the given a 64-bit steamid. Returns nil if not found
	-- @param string steamid 64-bit steam id to search for
	-- @return Player? The player with matching steamid
	function find_library.playerBySteamID64(steamid)
		local found = player.GetBySteamID64(steamid)
		if found then
			return plywrap(found)
		end
	end

	--- Returns entity that has given Entity:mapCreationID.
	-- @param number num Entity's creation id
	-- @return Entity? The found entity or nil if not found
	function find_library.getMapCreatedEntity(num)
		checkluatype(num, TYPE_NUMBER)
		return owrap(ents.GetMapCreatedEntity(num))
	end
end
