-- Global to all starfalls
local checkluatype = SF.CheckLuaType
local registerprivilege = SF.Permissions.registerPrivilege
local ENT_META = FindMetaTable("Entity")
local NPC_META = FindMetaTable("NPC")

local lagCompensated
if SERVER then
	-- Register privileges
	registerprivilege("npcs.modify", "Modify", "Allows the user to modify npcs", { entities = {} })
	registerprivilege("npcs.giveweapon", "Give weapon", "Allows the user to give npcs weapons", { entities = {} })

	lagCompensated = SF.EntManager(
		"npcs_lag_compensated",
		"lag compensated npcs",
		40,
		"The number of npcs allowed to be lag compensated"
	)
end

--- Npc type
-- @name Npc
-- @class type
-- @libtbl npc_methods
-- @libtbl npc_meta
SF.RegisterType("Npc", false, true, NPC_META, "Entity")

return function(instance)
	local checkpermission = instance.player ~= SF.Superuser and SF.Permissions.check or function() end
	local Ent_GetClass, Ent_IsLagCompensated, Ent_IsValid, Ent_Remove, Ent_SetLagCompensated =
		ENT_META.GetClass, ENT_META.IsLagCompensated, ENT_META.IsValid, ENT_META.Remove, ENT_META.SetLagCompensated
	local Npc_AddEntityRelationship, Npc_AddRelationship, Npc_GetEnemy, Npc_Give, Npc_SetLastPosition, Npc_SetSchedule, Npc_SetTarget =
		NPC_META.AddEntityRelationship,
		NPC_META.AddRelationship,
		NPC_META.GetEnemy,
		NPC_META.Give,
		NPC_META.SetLastPosition,
		NPC_META.SetSchedule,
		NPC_META.SetTarget

	local owrap, ounwrap = instance.WrapObject, instance.UnwrapObject
	local npc_methods, npc_meta, wrap, unwrap =
		instance.Types.Npc.Methods, instance.Types.Npc, instance.Types.Npc.Wrap, instance.Types.Npc.Unwrap
	local ent_meta, ewrap, eunwrap = instance.Types.Entity, instance.Types.Entity.Wrap, instance.Types.Entity.Unwrap
	local vec_meta, vwrap, vunwrap = instance.Types.Vector, instance.Types.Vector.Wrap, instance.Types.Vector.Unwrap

	local getent
	local vunwrap1
	instance:AddHook("initialize", function()
		getent = ent_meta.GetEntity
		npc_meta.__tostring = ent_meta.__tostring
		vunwrap1 = vec_meta.QuickUnwrap1
	end)

	instance:AddHook("deinitialize", function()
		if SERVER then
			lagCompensated:deinitialize(instance)
		end
	end)

	local function getnpc(self)
		local ent = npc_meta.sf2sensitive[self]
		if Ent_IsValid(ent) then
			return ent
		else
			SF.Throw("Entity is not valid.", 3)
		end
	end

	if SERVER then
		--- Sets an npc's hitboxes to compensate for lag, but limited number of npcs can be set due to high processing needed
		-- @server
		-- @param boolean compensate Whether to make an npc's hitboxes compensate lag
		function npc_methods:setLagCompensated(compensate)
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			if compensate and not Ent_IsLagCompensated(npc) then
				lagCompensated:checkuse(instance.player, 1)
				lagCompensated:register(instance, npc)
				Ent_SetLagCompensated(npc, true)
			elseif not compensate and Ent_IsLagCompensated(npc) then
				lagCompensated:unregister(npc)
				Ent_SetLagCompensated(npc, false)
			end
		end

		--- Gets whether an npc is lag compensated
		-- @server
		-- @return boolean Whether the npc is lag compensated
		function npc_methods:isLagCompensated()
			return Ent_IsLagCompensated(getnpc(self))
		end

		--- Adds a relationship to the npc
		-- @server
		-- @param string str The relationship string. http://wiki.facepunch.com/gmod/NPC:AddRelationship
		function npc_methods:addRelationship(str)
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			Npc_AddRelationship(npc, str)
		end

		local dispositions = {
			error = D_ER,
			hate = D_HT,
			fear = D_FR,
			like = D_LI,
			neutral = D_NU,
			[D_ER] = "error",
			[D_HT] = "hate",
			[D_FR] = "fear",
			[D_LI] = "like",
			[D_NU] = "neutral",
		}
		--- Adds a relationship to the npc with an entity
		-- @server
		-- @param Entity ent The target entity
		-- @param string disp String of the relationship. ("hate", "fear", "like", "neutral")
		-- @param number priority How strong the relationship is. Higher number is stronger
		function npc_methods:addEntityRelationship(ent, disp, priority)
			local npc = getnpc(self)
			local target = getent(ent)
			local relation = dispositions[disp]
			if not relation then
				SF.Throw("Invalid relationship specified", 2)
			end
			checkpermission(instance, npc, "npcs.modify")
			Npc_AddEntityRelationship(npc, target, relation, priority)
		end

		--- Gets the npc's relationship to the target
		-- @server
		-- @param Entity ent Target entity
		-- @return string Relationship of the npc with the target
		function npc_methods:getRelationship(ent)
			return dispositions[getnpc(self):Disposition(getent(ent))]
		end

		--- Gives the npc a weapon
		-- @server
		-- @param string wep The classname of the weapon
		function npc_methods:giveWeapon(wep)
			checkluatype(wep, TYPE_STRING)

			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.giveweapon")

			local weapon = npc:GetActiveWeapon()
			if Ent_IsValid(weapon) then
				if Ent_GetClass(weapon) == "weapon_" .. wep then
					return
				end
				Ent_Remove(weapon)
			end

			Npc_Give(npc, "ai_weapon_" .. wep)
		end

		--- Tell the npc to fight this
		-- @server
		-- @param Entity ent Target entity
		function npc_methods:setEnemy(ent)
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			Npc_SetTarget(npc, getent(ent))
		end

		--- Gets what the npc is fighting
		-- @server
		-- @return Entity Entity the npc is fighting
		function npc_methods:getEnemy()
			return owrap(Npc_GetEnemy(getnpc(self)))
		end

		--- Stops the npc
		-- @server
		function npc_methods:stop()
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			Npc_SetSchedule(npc, SCHED_NONE)
		end

		--- Makes the npc do a melee attack
		-- @server
		function npc_methods:attackMelee()
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			Npc_SetSchedule(npc, SCHED_MELEE_ATTACK1)
		end

		--- Makes the npc do a ranged attack
		-- @server
		function npc_methods:attackRange()
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			Npc_SetSchedule(npc, SCHED_RANGE_ATTACK1)
		end

		--- Makes the npc walk to a destination
		-- @server
		-- @param Vector vec The position of the destination
		function npc_methods:goWalk(vec)
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			Npc_SetLastPosition(npc, vunwrap1(vec))
			Npc_SetSchedule(npc, SCHED_FORCED_GO)
		end

		--- Makes the npc run to a destination
		-- @server
		-- @param Vector vec The position of the destination
		function npc_methods:goRun(vec)
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			Npc_SetLastPosition(npc, vunwrap1(vec))
			Npc_SetSchedule(npc, SCHED_FORCED_GO_RUN)
		end

		--- Returns the eye direction of the NPC.
		-- @server
		-- @return Vector The eye direction
		function npc_methods:getEyeDirection()
			return vwrap(getnpc(self):GetEyeDirection())
		end

		--- Returns the 2D head direction of the NPC.
		-- @server
		-- @return Vector The head direction
		function npc_methods:getHeadDirection()
			local headDirection = getnpc(self):GetHeadDirection()
			headDirection[3] = 0 -- This is garbage data

			return vwrap(headDirection)
		end

		--- Returns whether the NPC is moving or not.
		-- @server
		-- @return boolean Whether the NPC is moving
		function npc_methods:isMoving()
			return getnpc(self):IsMoving()
		end

		--- Sets the Field Of View of the NPC, for use with such functions as NPC:IsInViewCone. it is also used internally by the NPC for enemy detection, etc.
		-- @server
		-- @param number fov The field of view, in degrees [0, 360]
		function npc_methods:setFOV(fov)
			local npc = getnpc(self)
			checkpermission(instance, npc, "npcs.modify")
			checkluatype(fov, TYPE_NUMBER)

			-- Follows observed behavior of SetFOV's internal limitations
			npc:SetFOV(math.Clamp(math.abs(fov), 0, 360))
		end

		--- Returns the Field Of View of the NPC. See NPC:setFOV.
		-- @server
		-- @return number The field of view, in degrees
		function npc_methods:getFOV()
			return getnpc(self):GetFOV()
		end
	end
end
