---
-- @class ENT
-- @desc Thrown knife entity
-- @section KnifeProjectile

if SERVER then
    AddCSLuaFile()
end

if CLIENT then
    ENT.PrintName = "knife_thrown"
    ENT.Icon = "vgui/ttt/icon_knife"
end

ENT.Type = "anim"
ENT.Model = Model("models/weapons/w_knife_t.mdl")

-- When true, score code considers us a weapon
ENT.Projectile = true

ENT.Stuck = false
ENT.Weaponised = false
ENT.CanHavePrints = false
ENT.IsSilent = true
ENT.CanPickup = false

ENT.WeaponID = AMMO_KNIFE

ENT.Damage = 50

---
-- @realm shared
function ENT:Initialize()
    self:SetModel(self.Model)
    self:PhysicsInit(SOLID_VPHYSICS)

    if SERVER then
        self:SetGravity(0.4)
        self:SetFriction(1.0)
        self:SetElasticity(0.45)

        self.StartPos = self:GetPos()

        self:NextThink(CurTime())
    end

    self.Weaponised = false
    self.Stuck = false
end

---
-- @param table other
-- @param table tr
-- @realm shared
function ENT:HitPlayer(other, tr)
    local range_dmg = math.max(self.Damage, self.StartPos:Distance(self:GetPos()) / 3)

    if other:Health() < range_dmg + 10 then
        self:KillPlayer(other, tr)
    elseif SERVER then
        local dmg = DamageInfo()
        dmg:SetDamage(range_dmg)
        dmg:SetAttacker(self:GetOwner())
        dmg:SetInflictor(self)
        dmg:SetDamageForce(self:EyeAngles():Forward())
        dmg:SetDamagePosition(self:GetPos())
        dmg:SetDamageType(DMG_SLASH)

        local ang = Angle(-28, 0, 0) + tr.Normal:Angle()
        ang:RotateAroundAxis(ang:Right(), -90)
        other:DispatchTraceAttack(dmg, self:GetPos() + ang:Forward() * 3, other:GetPos())

        if not self.Weaponised then
            self:BecomeWeaponDelayed()
        end
    end

    -- As a thrown knife, after we hit a target we can never hit one again.
    -- If we are picked up and re-thrown, a new knife_proj entity is created.
    -- To make sure we can never deal damage twice, make HitPlayer do nothing.
    self.HitPlayer = util.noop
end

---
-- @param table other
-- @param table tr
-- @realm shared
function ENT:KillPlayer(other, tr)
    local dmg = DamageInfo()
    dmg:SetDamage(2000)
    dmg:SetAttacker(self:GetOwner())
    dmg:SetInflictor(self)
    dmg:SetDamageForce(self:EyeAngles():Forward())
    dmg:SetDamagePosition(self:GetPos())
    dmg:SetDamageType(DMG_SLASH)

    -- this bone is why we need the trace
    local bone = tr.PhysicsBone
    local pos = tr.HitPos
    local norm = tr.Normal
    local ang = Angle(-28, 0, 0) + norm:Angle()

    ang:RotateAroundAxis(ang:Right(), -90)
    pos = pos - (ang:Forward() * 8)

    local knife = self
    local prints = self.fingerprints

    other.effect_fn = function(rag)
        if not IsValid(knife) or not IsValid(rag) then
            return
        end

        knife:SetPos(pos)
        knife:SetCollisionGroup(COLLISION_GROUP_DEBRIS)
        knife:SetAngles(ang)

        knife:SetMoveCollide(MOVECOLLIDE_DEFAULT)
        knife:SetMoveType(MOVETYPE_VPHYSICS)

        knife.fingerprints = prints
        knife:SetNWBool("HasPrints", true)

        -- knife needs to be trace-able to get prints
        local phys = knife:GetPhysicsObject()
        if IsValid(phys) then
            phys:EnableCollisions(false)
        end

        constraint.Weld(rag, knife, bone, 0, 0, true)

        rag:CallOnRemove("ttt_knife_cleanup", function()
            SafeRemoveEntity(knife)
        end)
    end

    other:DispatchTraceAttack(dmg, self:GetPos() + ang:Forward() * 3, other:GetPos())

    self.Stuck = true
end

if SERVER then
    ---
    -- @realm server
    function ENT:Think()
        if self.Stuck then
            return
        end

        local vel = self:GetVelocity()

        if vel == vector_origin then
            return
        end
        local tr = util.TraceLine({
            start = self:GetPos(),
            endpos = self:GetPos() + vel:GetNormalized() * 20,
            filter = { self, self:GetOwner() },
            mask = MASK_SHOT_HULL,
        })

        if tr.Hit and tr.HitNonWorld and IsValid(tr.Entity) then
            local other = tr.Entity
            if other:IsPlayer() then
                self:HitPlayer(other, tr)
            end
        end

        self:NextThink(CurTime())

        return true
    end

    ---
    -- When this entity touches anything that is not a player, it should turn into a
    -- weapon ent again. If it touches a player it sticks in it.
    -- @realm server
    function ENT:BecomeWeapon()
        self.Weaponised = true

        local wep = ents.Create("weapon_ttt_knife")
        wep:SetPos(self:GetPos())
        wep:SetAngles(self:GetAngles())
        wep.IsDropped = true

        wep.fingerprints = table.Copy(self.fingerprints or {})

        self:Remove()

        wep:Spawn()

        return wep
    end

    ---
    -- @realm server
    function ENT:BecomeWeaponDelayed()
        -- delay the weapon-replacement a tick because Source gets very angry
        -- if you do fancy stuff in a physics callback
        local knife = self
        timer.Simple(0, function()
            if not IsValid(knife) or knife.Weaponised then
                return
            end

            knife:BecomeWeapon()
        end)
    end

    ---
    -- @param table data
    -- @param table phys
    -- @realm server
    function ENT:PhysicsCollide(data, phys)
        if self.Stuck then
            return false
        end

        local other = data.HitEntity

        if not IsValid(other) and not other:IsWorld() then
            return
        end

        if other:IsPlayer() then
            local tr = util.TraceLine({
                start = self:GetPos(),
                endpos = other:LocalToWorld(other:OBBCenter()),
                filter = { self, self:GetOwner() },
                mask = MASK_SHOT_HULL,
            })

            if tr.Hit and tr.Entity == other then
                self:HitPlayer(other, tr)
            end

            return true
        end

        if not self.Weaponised then
            self:BecomeWeaponDelayed()
        end
    end
end
