local meta = FindMetaTable("Entity")

--[[local o = meta.SetCollisionGroup
function meta:SetCollisionGroup(g)
	print(self, g)
	debug.Trace()
	o(self, g)
end]]

--[[
	From the Garry's Mod Wiki (https://wiki.facepunch.com/gmod/Entity:GetSaveTable):

	It is highly recommended to use Entity:GetInternalVariable for retrieving a single
	key of the save table for performance reasons.
--]]

function meta:IsDoorLocked()
	return self:GetInternalVariable("m_bLocked")
end

-- Figured "Natively" is a must when just about everything in this gamemode is destructible
-- but through self-implemented means (not mechanics "native" to the game engine)
function meta:IsDoorNativelyBreakable()
	return self:GetInternalVariable("m_bBreakable") -- This can also change through entity I/O
end

function meta:HealPlayer(pl, amount, pointmul, nobymsg, poisononly)
	local healed, rmv = 0, 0

	local health, maxhealth = pl:Health(), pl:IsSkillActive(SKILL_D_FRAIL) and math.floor(pl:GetMaxHealth() * 0.25) or pl:GetMaxHealth()
	local missing_health = maxhealth - health
	local poison = pl:GetPoisonDamage()
	local bleed = pl:GetBleedDamage()

	local healrec = (pl.HealingReceived or 1) - (pl:GetPhantomHealth() > 0.5 and 0.5 or 0) - (pl:GetStatus("sickness") and 0.5 or 0)
	local healmul = self.MedicHealMul or 1
	local multiplier = healmul + healrec - 1
	local regamount = healmul * amount

	amount = amount * multiplier

	-- Heal bleed first.
	if not poisononly and bleed > 0 then
		rmv = math.min(amount, bleed)
		pl:AddBleedDamage(-rmv)
		healed = healed + rmv
		amount = amount - rmv
	end

	-- Heal poison next.
	if poison > 0 and amount > 0 then
		rmv = math.min(amount, poison)
		pl:AddPoisonDamage(-rmv)
		healed = healed + rmv
		amount = amount - rmv
	end

	-- Then heal missing health.
	if not poisononly and missing_health > 0 and amount > 0 then
		rmv = math.min(amount, missing_health)
		pl:SetHealth(health + rmv)
		healed = healed + rmv
		amount = amount - rmv
	end

	pointmul = (pointmul or 1) / (math.max(healed, regamount) / regamount)

	if healed > 0 and self:IsPlayer() then
		gamemode.Call("PlayerHealedTeamMember", self, pl, healed, self:GetActiveWeapon(), pointmul, nobymsg, healed >= 10)
		pl:SetPhantomHealth(math.max(0, pl:GetPhantomHealth() - healed))
	end

	return healed
end

local healthpropscalar = {
	["models/props_c17/door01_left.mdl"] = 0.7
}

function meta:GetDefaultBarricadeHealth()
	local mass = 2
	if self._OriginalMass then
		mass = self._OriginalMass
	else
		local phys = self:GetPhysicsObject()
		if phys:IsValid() then
			mass = phys:GetMass()
		end
	end

	local mdl = string.lower(self:GetModel())
	local scalar = healthpropscalar[mdl] or 1

	return math.Clamp((mass * GAMEMODE.BarricadeHealthMassFactor + self:GetVolume() * GAMEMODE.BarricadeHealthVolumeFactor) * scalar, GAMEMODE.BarricadeHealthMin, GAMEMODE.BarricadeHealthMax)
end

function meta:HitFence(data, phys)
	local pos = phys:GetPos()
	local vel = data.OurOldVelocity
	local endpos = data.HitPos + vel:GetNormalized()
	if util.TraceLine({start = pos, endpos = endpos, mask = MASK_SOLID, filter = self}).Hit and not util.TraceLine({start = pos, endpos = endpos, mask = MASK_SHOT, filter = self}).Hit then -- Essentially hit a fence or passable object.
		self:SetPos(data.HitPos)
		phys:SetPos(data.HitPos)
		phys:SetVelocityInstantaneous(vel)

		return true
	end

	return false
end

function meta:FakePropBreak()
	local ent = ents.Create("prop_physics")
	if ent:IsValid() then
		ent:SetModel(self:GetModel())
		ent:SetMaterial(self:GetMaterial())
		ent:SetAngles(self:GetAngles())
		ent:SetPos(self:GetPos())
		ent:SetSkin(self:GetSkin() or 0)
		ent:SetColor(self:GetColor())
		ent:Spawn()
		ent:Fire("break", "", 0)
		ent:Fire("kill", "", 0.1)
	end
end

function meta:SetBarricadeHealth(m)
	self:SetDTFloat(1, m)
end

function meta:SetMaxBarricadeHealth(m)
	self:SetDTFloat(2, m)
end

function meta:SetBarricadeRepairs(m)
	self:SetDTFloat(3, m)
end

function meta:GhostAllPlayersInMe(timeout, allowrepeat)
	if not allowrepeat then
		if self.GhostedBefore then return end
		self.GhostedBefore = true
	end

	if self.PreHoldCollisionGroup and self.PreHoldCollisionGroup == COLLISION_GROUP_DEBRIS_TRIGGER then return end -- No need to ghost.

	local ent = ents.Create("point_propnocollide")
	if ent:IsValid() then
		ent:SetPos(self:GetPos())
		ent:Spawn()
		if timeout then
			ent:SetTimeOut(CurTime() + timeout)
		end
		ent:SetTeam(TEAM_HUMAN)

		ent:SetProp(self)

		return ent
	end
end

function meta:AddUselessDamage(damage)
	self.UselessDamage = (self.UselessDamage or 0) + damage
end

function meta:RemoveUselessDamage(damage)
	if self.UselessDamage then
		damage = math.min(self.UselessDamage, damage)
		self.UselessDamage = self.UselessDamage - damage

		return damage
	end

	return 0
end

function meta:ClearUselessDamage()
	self.UselessDamage = nil
end

local function SortItems(a, b)
	if a.CleanupPriority == b.CleanupPriority then
		return a.Created < b.Created
	end

	return a.CleanupPriority < b.CleanupPriority
end
local function CheckItemCreated(self)
	if not self:IsValid() or self.PlacedInMap then return end

	local tab = {}
	for _, ent in pairs(ents.FindByClass("prop_ammo")) do
		if not ent.PlacedInMap then
			table.insert(tab, ent)
		end
	end
	for _, ent in pairs(ents.FindByClass("prop_weapon")) do
		if not ent.PlacedInMap then
			table.insert(tab, ent)
		end
	end

	if #tab > GAMEMODE.MaxDroppedItems then
		table.sort(tab, SortItems)
		for i = 1, GAMEMODE.MaxDroppedItems do
			tab[i]:Remove()
		end
	end
end
function meta:ItemCreated()
	self.Created = self.Created or CurTime()
	timer.Simple(0, function() CheckItemCreated(self) end)
end

function meta:FireOutput(outpt, activator, caller, args)
	local intab = self[outpt]
	if intab then
		for key, tab in pairs(intab) do
			local param = ((tab.args == "") and args) or tab.args
			for __, subent in pairs(self:FindByNameHammer(tab.entityname, activator, caller)) do
				local delay = tonumber(tab.delay)
				if delay == nil or delay <= 0 then
					subent:Input(tab.input, activator, caller, param)
				else
					local inp = tab.input
					timer.Simple(delay, function() if subent:IsValid() then subent:Input(inp, activator, caller, param) end end)
				end
			end
		end
	end
end

function meta:AddOnOutput(key, value)
	self[key] = self[key] or {}

	-- Newer Source Engine games use this symbol as a delimiter
	local tab = string.Explode("\x1B", value)
	-- Fall back to comma-delimited array when necessary
	if (#tab < 2) then
		tab = string.Explode(",", value)
	end

	table.insert(self[key], {entityname=tab[1], input=tab[2], args=tab[3], delay=tab[4], reps=tab[5]})
end

function meta:FindByNameHammer(name, activator, caller)
	if name == "!self" then return {self} end
	if name == "!activator" then return {activator} end
	if name == "!caller" then return {caller} end
	return ents.FindByName(name)
end

function meta:IsNailed()
	if self:IsValid() and self.Nails then -- In case we're the world.
		for _, nail in pairs(self.Nails) do
			if nail and nail:IsValid() and (nail:GetAttachEntity() == self or nail:GetBaseEntity() == self) then
				return true
			end
		end
	end

	return false
end

function meta:IsNailedToWorld(hierarchy)
	if self:IsNailed() then
		for _, nail in pairs(self.Nails) do
			if nail:GetAttachEntity():IsWorld() then
				return true
			end
		end
	end

	if hierarchy then
		for _, ent in pairs(self:GetAllConstrainedEntities()) do
			if ent ~= self and ent:IsValid() and ent:IsNailedToWorld() then return true end
		end
	end

	return false
end

function meta:IsNailedToWorldHierarchy()
	return self:IsNailedToWorld(true)
end

function meta:GetNailFrozen()
	return self.m_NailFrozen
end
meta.IsNailFrozen = meta.GetNailFrozen

function meta:SetNailFrozen(frozen)
	if frozen then
		local phys = self:GetPhysicsObject()
		if phys:IsValid() and phys:IsMoveable() then
			self.m_NailFrozen = true
			phys:EnableMotion(false)
		end
	elseif self:IsNailFrozen() then
		local phys = self:GetPhysicsObject()
		if phys:IsValid() then
			self.m_NailFrozen = false
			phys:EnableMotion(true)
			phys:Wake()
		end
	end
end

function constraint.GetAllConstrainedEntitiesOrdered(ent)
	local allcons = constraint.GetAllConstrainedEntities(ent)

	local tab = {}

	if allcons then
		for k, v in pairs(allcons) do
			table.insert(tab, v)
		end
	end

	return tab
end

function meta:GetAllConstrainedEntities()
	local allcons = constraint.GetAllConstrainedEntitiesOrdered(self)
	if not allcons or #allcons == 0 then
		return {self}
	end

	return allcons
end

function meta:PackUp(pl)
	if not self.CanPackUp then return end

	local cur = pl:GetStatus("packup")
	if cur and cur:IsValid() then return end

	local status = pl:GiveStatus("packup")
	if status:IsValid() then
		status:SetPackUpEntity(self)
		status:SetEndTime(CurTime() + (self.PackUpTime or 3) * (not self.IgnorePackTimeMul and pl.DeployablePackTimeMul or 1))

		if self.GetObjectOwner then
			local owner = self:GetObjectOwner()
			if owner:IsValid() and owner:Team() == TEAM_HUMAN and owner ~= pl and not gamemode.Call("PlayerIsAdmin", pl) then
				status:SetNotOwner(true)
			end
		end
	end
end

function meta:GetPropsInContraption()
	local allcons = constraint.GetAllConstrainedEntities(self)
	if not allcons or #allcons == 0 then
		return 1
	end

	return #allcons
end

function meta:HumanNearby()
	for _, pl in pairs(team.GetPlayers(TEAM_HUMAN)) do
		if pl:Alive() and self:GetPos():DistToSqr(pl:GetPos()) <= 262144 then -- 512^2
			return true
		end
	end
end

function meta:ResetLastBarricadeAttacker(attacker, dmginfo)
	if attacker:IsPlayer() and attacker:Team() == TEAM_UNDEAD then
		self.m_LastDamagedByZombie = CurTime()

		if self:HumanNearby() then
			local dmg = math.ceil(dmginfo:GetDamage())
			attacker.BarricadeDamage = attacker.BarricadeDamage + dmg
			if attacker.LifeBarricadeDamage ~= nil then
				attacker:AddLifeBarricadeDamage(dmg)
				GAMEMODE.StatTracking:IncreaseElementKV(STATTRACK_TYPE_ZOMBIECLASS, attacker:GetZombieClassTable().Name, "BarricadeDamage", dmg)
			end
		end
	end
end

meta.OldSetPhysicsAttacker = meta.SetPhysicsAttacker
function meta:SetPhysicsAttacker(ent)
	if string.sub(self:GetClass(), 1, 12) == "func_physbox" and ent:IsValid() then
		self.PBAttacker = ent
		self.NPBAttacker = CurTime() + 5
	end
	self:OldSetPhysicsAttacker(ent)
end

-- Return true to override default behavior.
function meta:DamageNails(attacker, inflictor, damage, dmginfo)
	if not self:IsNailed() or self.m_NailsDontAbsorb then return end

	-- Props that don't have barricade health yet might still be nailed to something.
	local nails = self:GetLivingNails()
	local isattach = false
	for i, nail in ipairs(nails) do
		isattach = self == nail:GetAttachEntity() or isattach
	end

	if self:GetBarricadeHealth() <= 0 and not isattach then return end

	if not gamemode.Call("CanDamageNail", self, attacker, inflictor, damage, dmginfo) then
		if dmginfo then
			dmginfo:SetDamage(0)
			dmginfo:SetDamageType(DMG_GENERIC)
		end

		return true
	end

	-- No physics damage to props. Stops ramming/thrown weapons damaging them.
	if damage < 0 or dmginfo:GetDamageType() == DMG_CRUSH then
		if dmginfo then
			dmginfo:SetDamage(0)
		end

		return true
	end

	if self.ReinforceEnd and CurTime() < self.ReinforceEnd and self.ReinforceApplier and self.ReinforceApplier:IsValidLivingHuman() then
		local applier = self.ReinforceApplier
		local multi = 0.92
		local dmgbefore = damage * 0.08
		local points = dmgbefore / 8

		dmginfo:SetDamage(dmginfo:GetDamage() * multi)
		damage = damage * multi

		applier.PropDef = (applier.PropDef or 0) + dmgbefore
		applier:AddPoints(points)
	end

	if gamemode.Call("IsEscapeDoorOpen") then
		local multi = gamemode.Call("GetEscapeStage") * 1.5

		dmginfo:SetDamage(dmginfo:GetDamage() * multi)
		damage = damage * multi
	end

	self:ResetLastBarricadeAttacker(attacker, dmginfo)

	if #nails <= 0 then return end

	if attacker:IsPlayer() then
		-- :O)
		if attacker.SpawnProtection then
			damage = damage * 5
			dmginfo:SetDamage(damage)
			self:AddUselessDamage(damage)
		end

		GAMEMODE:DamageFloater(attacker, self, dmginfo:GetDamagePosition(), dmginfo:GetDamage())
	end

	self:SetBarricadeHealth(self:GetBarricadeHealth() - damage)
	for i, nail in ipairs(nails) do
		nail:OnDamaged(damage, attacker, inflictor, dmginfo)
	end

	-- No points for repairing damage from fire, trigger_hurt, etc.
	if not attacker:IsZombie() then
		self:AddUselessDamage(damage)
	end

	attacker.LastBarricadeHit = CurTime()

	if dmginfo then dmginfo:SetDamage(0) end

	if self:GetBarricadeHealth() <= 0 then
		if self:GetModel() ~= "" and self:GetModel() ~= "models/error.mdl" then
			if self:GetName() == "" and self:GetVolume() < 100 then
				self:Fire("break", "", 0.01)
				self:Fire("kill", "", 0.05)
			else
				local ent = ents.Create("env_propbroken")
				if ent:IsValid() then
					ent:Spawn()
					ent:AttachTo(self)
				end
			end
		end

		for _, nail in pairs(nails) do
			self:RemoveNail(nail, nil, nil, true)
		end
	end

	return true
end

function meta:GetNails()
	local tab = {}

	if self.Nails then
		for _, nail in pairs(self.Nails) do
			if nail and nail:IsValid() then
				table.insert(tab, nail)
			end
		end
	end

	return tab
end

function meta:GetLivingNails()
	local tab = {}

	if self.Nails then
		for _, nail in pairs(self.Nails) do
			if nail and nail:IsValid() and nail:GetNailHealth() > 0 then
				table.insert(tab, nail)
			end
		end
	end

	return tab
end

function meta:NumLivingNails()
	local amount = 0

	if self.Nails then
		for _, nail in pairs(self.Nails) do
			if nail and nail:IsValid() and nail:GetNailHealth() > 0 then
				amount = amount + 1
			end
		end
	end

	return amount
end

function meta:GetFirstNail()
	if self.Nails then
		for i, nail in ipairs(self.Nails) do
			if nail and nail:IsValid() and not nail:GetAttachEntity():IsValid() then return nail end
		end
		for i, nail in ipairs(self.Nails) do
			if nail and nail:IsValid() then return nail end
		end
	end
end

local function GetNailOwner(nail, filter)
	for _, ent in pairs(ents.GetAll()) do
		if ent and ent ~= filter and ent.Nails and ent:IsValid() then
			for __, n in pairs(ent.Nails) do
				if n == nail then
					return ent
				end
			end
		end
	end

	return game.GetWorld()
end

function meta:RemoveNail(nail, dontremoveentity, removedby, forceremoveconstraint)
	if not self:IsNailed() then return end

	if not nail then
		nail = self:GetFirstNail()
	end

	if not nail or not nail:IsValid() then return end

	local cons = nail:GetNailConstraint()
	local othernails = 0
	if not forceremoveconstraint then
		for _, othernail in pairs(ents.FindByClass("prop_nail")) do
			if othernail ~= nail and not nail.m_IsRemoving and othernail:GetNailConstraint():IsValid() and othernail:GetNailConstraint() == cons then
				othernails = othernails + 1
			end
		end
	end

	-- Only remove the constraint if it's the last nail.
	if othernails == 0 and cons:IsValid() then
		if self.PropHealth and self:GetBarricadeHealth() > 0 then
			local repairs_frac = self:GetBarricadeRepairs() / self:GetMaxBarricadeRepairs()

			if repairs_frac < 0.5 then
				self.PropHealth = math.min(self.PropHealth, self:GetBarricadeHealth())

				local brit = math.Clamp(self.PropHealth / self.TotalHealth, 0, 1)
				local col = self:GetColor()
				col.r = 255
				col.g = 255 * brit
				col.b = 255 * brit
				self:SetColor(col)
			end
		end
		cons:Remove()
	end

	local ent2 = GetNailOwner(nail, self)

	for i, n in ipairs(self.Nails) do
		if n == nail then
			table.remove(self.Nails, i)
			break
		end
	end

	if ent2 and ent2.Nails then
		for i, n in ipairs(ent2.Nails) do
			if n == nail then
				table.remove(ent2.Nails, i)
				ent2:TemporaryBarricadeObject()
				break
			end
		end
	end

	self:TemporaryBarricadeObject()

	gamemode.Call("OnNailRemoved", nail, self, ent2, removedby)

	if not dontremoveentity then
		nail:Remove()
		nail.m_IsRemoving = true
	end

	self:RecalculateNailBonuses()
	self:CollisionRulesChanged()

	if ent2 and ent2:IsValid() then
		ent2:CollisionRulesChanged()
	end

	return true
end

function meta:RemoveNextFrame(time)
	self.Removing = true
	self:Fire("kill", "", time or 0.01)
end

local function barricadetimer(self, timername)
	if self:IsValid() then
		for _, e in pairs(ents.FindInBox(self:WorldSpaceAABB())) do
			if e and e:IsValid() and e:IsPlayer() and e:Alive() then
				return
			end
		end

		self.IsBarricadeObject = nil
		self:CollisionRulesChanged()
	end

	timer.Remove(timername)
end
function meta:TemporaryBarricadeObject()
	if self.IsBarricadeObject then return end

	for _, e in pairs(ents.FindInBox(self:WorldSpaceAABB())) do
		if e and e:IsValid() and e:IsPlayer() and e:Alive() then
			self.IsBarricadeObject = true
			self:CollisionRulesChanged()

			local timername = "TemporaryBarricadeObject"..self:GetCreationID()
			timer.Create(timername, 0, 0, function() barricadetimer(self, timername) end)

			return
		end
	end
end

function meta:RecalculateNailBonuses()
	local max_health = self:GetMaxBarricadeHealth()
	if max_health == 0 then return end

	local num_extra_nails = math.Clamp(self:NumLivingNails() - 1, 0, 3)
	local repairs_frac = self:GetBarricadeRepairs() / self:GetMaxBarricadeRepairs()

	self.OriginalMaxHealth = self.OriginalMaxHealth or max_health
	self.OriginalMaxBarricadeRepairs = self.OriginalMaxBarricadeRepairs or max_repairs

	local health = self:GetBarricadeHealth()
	local new_max_health = self.OriginalMaxHealth + num_extra_nails * GAMEMODE.ExtraHealthPerExtraNail
	self:SetMaxBarricadeHealth(new_max_health)
	self:SetBarricadeHealth(health / max_health * new_max_health)

	self:SetBarricadeRepairs(repairs_frac * self:GetMaxBarricadeRepairs())
end

function meta:SetupDeployableSkillHealth(extramodifier)
	local owner = self:GetObjectOwner()
	local newmaxhealth = self.MaxHealth or self:GetMaxObjectHealth()
	local currentmaxhealth = self:GetMaxObjectHealth()

	if owner:IsValid() then
		newmaxhealth = newmaxhealth * owner:GetTotalAdditiveModifier("DeployableHealthMul", extramodifier)
	end

	newmaxhealth = math.ceil(newmaxhealth)
	self:SetMaxObjectHealth(newmaxhealth)
	self:SetObjectHealth(self:GetObjectHealth() / currentmaxhealth * newmaxhealth)
end

function meta:DealProjectileTraceDamage(damage, tr, owner)
	local ent = tr.Entity

	local damageinfo = DamageInfo()
	damageinfo:SetDamageType(DMG_BULLET)
	damageinfo:SetDamage(damage)
	damageinfo:SetDamagePosition(tr.HitPos)
	damageinfo:SetAttacker(owner)
	damageinfo:SetInflictor(self:ProjectileDamageSource())

	local vel
	if ent:IsPlayer() then
		ent:SetLastHitGroup(tr.HitGroup)
		if tr.HitGroup == HITGROUP_HEAD then
			ent:SetWasHitInHead()
		end

		vel = ent:GetVelocity()
	end

	ent:DispatchTraceAttack(damageinfo, tr, tr.Normal)

	if vel and ent:IsPlayer() and owner ~= ent then
		ent:SetLocalVelocity(vel)
	end
end

GM.ProjectileThickness = 3

function meta:ProjectileTraceAhead(phys)
	if not self.Touched then
		local vel = self.PreVel or phys:GetVelocity()
		if self.PreVel then self.PreVel = nil end

		local velnorm = vel:GetNormalized()

		local ahead = (vel:LengthSqr() * FrameTime()) / 1200
		local fwd = velnorm * ahead
		local start = self:GetPos() - fwd
		local side = vel:Angle():Right() * GAMEMODE.ProjectileThickness

		local proj_trace = {mask = MASK_SHOT, filter = {self, team.GetPlayers(TEAM_HUMAN)}}

		proj_trace.start = start - side
		proj_trace.endpos = start - side + fwd

		local tr = util.TraceLine(proj_trace)

		proj_trace.start = start + side
		proj_trace.endpos = start + side + fwd

		local tr2 = util.TraceLine(proj_trace)
		local trs = {tr, tr2}

		for _, trace in pairs(trs) do
			if trace.Entity then
				local ent = trace.Entity

				if ent:IsValidLivingZombie() or ent.ZombieConstruction then
					self.Touched = trace
				end

				break
			end
		end
	end
end

-- Cache invisible entities every so often, for TrueVisible functions. Fixes a few issues and improves performance.
GM.CachedInvisibleEntities = {}
timer.Create("CachedInvisibleEntities", 1, 0, function()
	if not GAMEMODE then return end
	GAMEMODE.CachedInvisibleEntities = {}

	local invisents = player.GetAll()
	for _, ent in pairs(ents.GetAll()) do
		if ent.IgnoreTraces or ent.NoBlockExplosions then
			invisents[#invisents + 1] = ent
		end
	end

	GAMEMODE.CachedInvisibleEntities = invisents
end)
