---- Corpse functions

-- namespaced because we have no ragdoll metatable
CORPSE = {}

include("corpse_shd.lua")

--- networked data abstraction layer
local dti = CORPSE.dti

function CORPSE.SetFound(rag, state)
   --rag:SetNWBool("found", state)
   rag:SetDTBool(dti.BOOL_FOUND, state)
end

function CORPSE.SetPlayerNick(rag, ply_or_name)
   -- don't have datatable strings, so use a dt entity for common case of
   -- still-connected player, and if the player is gone, fall back to nw string
   local name = ply_or_name
   if IsValid(ply_or_name) then
      name = ply_or_name:Nick()
      rag:SetDTEntity(dti.ENT_PLAYER, ply_or_name)
   end

   rag:SetNWString("nick", name)
end

function CORPSE.SetCredits(rag, credits)
   --rag:SetNWInt("credits", credits)
   rag:SetDTInt(dti.INT_CREDITS, credits)
end


--- ragdoll creation and search

-- If detective mode, announce when someone's body is found
local bodyfound = CreateConVar("ttt_announce_body_found", "1")

function GM:TTTCanIdentifyCorpse(ply, corpse, was_traitor)
   -- return true to allow corpse identification, false to disallow
   return true
end

local function IdentifyBody(ply, rag)
   if not ply:IsTerror() then return end

   -- simplified case for those who die and get found during prep
   if GetRoundState() == ROUND_PREP then
      CORPSE.SetFound(rag, true)
      return
   end

   if not hook.Run("TTTCanIdentifyCorpse", ply, rag, (rag.was_role == ROLE_TRAITOR)) then
      return
   end

   local finder = ply:Nick()
   local nick = CORPSE.GetPlayerNick(rag, "")
   local traitor = (rag.was_role == ROLE_TRAITOR)

   -- Announce body
   if bodyfound:GetBool() and not CORPSE.GetFound(rag, false) then
      local roletext = nil
      local role = rag.was_role
      if role == ROLE_TRAITOR then
         roletext = "body_found_t"
      elseif role == ROLE_DETECTIVE then
         roletext = "body_found_d"
      else
         roletext = "body_found_i"
      end

      LANG.Msg("body_found", {finder = finder,
                              victim = nick,
                              role = LANG.Param(roletext)})
   end

   -- Register find
   if not CORPSE.GetFound(rag, false) then
      -- will return either false or a valid ply
      local deadply = player.GetBySteamID64(rag.sid64)
      if deadply then
         deadply:SetNWBool("body_found", true)

         if traitor then
            -- update innocent's list of traitors
            SendConfirmedTraitors(GetInnocentFilter(false))
         end
         SCORE:HandleBodyFound(ply, deadply)
      end
      hook.Call( "TTTBodyFound", GAMEMODE, ply, deadply, rag )
      CORPSE.SetFound(rag, true)
   end

   -- Handle kill list
   for k, vicsid64 in ipairs(rag.kills) do
      -- filter out disconnected
      local vic = player.GetBySteamID64(vicsid64)

      -- is this an unconfirmed dead?
      if IsValid(vic) and (not vic:GetNWBool("body_found", false)) then
         LANG.Msg("body_confirm", {finder = finder, victim = vic:Nick()})

         -- update scoreboard status
         vic:SetNWBool("body_found", true)
      end
   end
end

-- Covert identify concommand for traitors
local function IdentifyCommand(ply, cmd, args)
   if not IsValid(ply) then return end
   if #args != 2 then return end

   local eidx = tonumber(args[1])
   local id = tonumber(args[2])
   if (not eidx) or (not id) then return end

   if (not ply.search_id) or ply.search_id.id != id or ply.search_id.eidx != eidx then
      ply.search_id = nil
      return
   end

   ply.search_id = nil

   local rag = Entity(eidx)
   if IsValid(rag) and rag.player_ragdoll and rag:GetPos():Distance(ply:GetPos()) < 128 then
      if not CORPSE.GetFound(rag, false) then
         IdentifyBody(ply, rag)
      end
   end
end
concommand.Add("ttt_confirm_death", IdentifyCommand)

-- Call detectives to a corpse
local function CallDetective(ply, cmd, args)
   if not IsValid(ply) then return end
   if #args != 1 then return end
   if not ply:IsActive() then return end

   local eidx = tonumber(args[1])
   if not eidx then return end

   local rag = Entity(eidx)
   if not (IsValid(rag) and rag.player_ragdoll) then return end

   if ((rag.last_detective_call or 0) < (CurTime() - 5)) and (rag:GetPos():Distance(ply:GetPos()) < 128) then

      rag.last_detective_call = CurTime()

      if CORPSE.GetFound(rag, false) then
         -- show indicator to detectives
         net.Start("TTT_CorpseCall")
            net.WriteVector(rag:GetPos())
         net.Send(GetDetectiveFilter(true))

         LANG.Msg("body_call", {player = ply:Nick(),
                                victim = CORPSE.GetPlayerNick(rag, "someone")})

      else
         LANG.Msg(ply, "body_call_error")
      end
   end
end
concommand.Add("ttt_call_detective", CallDetective)

local bitsRequired = util.BitsRequired

local plyBits = bitsRequired(game.MaxPlayers()) -- first game.MaxPlayers() of entities are for players.

function GM:TTTCanSearchCorpse(ply, corpse, is_covert, is_long_range, was_traitor)
   -- return true to allow corpse search, false to disallow.
   return true
end

-- Send a usermessage to client containing search results
function CORPSE.ShowSearch(ply, rag, covert, long_range)
   if not IsValid(ply) or not IsValid(rag) then return end

   if rag:IsOnFire() then
      LANG.Msg(ply, "body_burning")
      return
   end

   if not hook.Run("TTTCanSearchCorpse", ply, rag, covert, long_range, (rag.was_role == ROLE_TRAITOR)) then
      return
   end

   -- init a heap of data we'll be sending
   local nick  = CORPSE.GetPlayerNick(rag)
   local traitor = (rag.was_role == ROLE_TRAITOR)
   local role  = rag.was_role
   local eq    = rag.equipment or EQUIP_NONE
   local c4    = rag.bomb_wire or 0
   local dmg   = rag.dmgtype or DMG_GENERIC
   local wep   = rag.dmgwep or ""
   local words = rag.last_words or ""
   local hshot = rag.was_headshot or false
   local dtime = rag.time or 0

   local owner = player.GetBySteamID64(rag.sid64)
   owner = IsValid(owner) and owner:EntIndex() or 0

   -- basic sanity check
   if nick == nil or eq == nil or role == nil then return end

   if DetectiveMode() and not covert then
      IdentifyBody(ply, rag)
   end

   local credits = CORPSE.GetCredits(rag, 0)
   if ply:IsActiveSpecial() and credits > 0 and (not long_range) then
      LANG.Msg(ply, "body_credits", {num = credits})
      ply:AddCredits(credits)

      CORPSE.SetCredits(rag, 0)

      ServerLog(ply:Nick() .. " took " .. credits .. " credits from the body of " .. nick .. "\n")
      SCORE:HandleCreditFound(ply, nick, credits)
   end

   -- time of death relative to current time (saves bits)
   if dtime != 0 then
      dtime = math.Round(CurTime() - dtime)
   end

   -- identifier so we know whether a ttt_confirm_death was legit
   ply.search_id = { eidx = rag:EntIndex(), id = rag:EntIndex() + dtime }

   -- time of dna sample decay relative to current time
   local stime = 0
   if rag.killer_sample then
      stime = math.max(0, rag.killer_sample.t - CurTime())
   end

   -- build list of people this traitor killed
   local kill_entids = {}
   for k, vicsid64 in ipairs(rag.kills) do
      -- also send disconnected players as a marker
      local vic = player.GetBySteamID64(vicsid64)
      table.insert(kill_entids, IsValid(vic) and vic:EntIndex() or 0)
   end

   local lastid = 0
   if rag.lastid and ply:IsActiveDetective() then
      -- if the person this victim last id'd has since disconnected, send 0 to
      -- indicate this
      lastid = IsValid(rag.lastid.ent) and rag.lastid.ent:EntIndex() or 0
   end

   -- Send a message with basic info
   net.Start("TTT_RagdollSearch")
      net.WriteUInt(rag:EntIndex(), 16) -- 16 bits
      net.WriteUInt(owner, plyBits) -- 128 max players. ( 8 bits )
      net.WriteString(nick)
      net.WriteInt(eq, bitsRequired(EQUIP_MAX, true)) -- Equipment ( default: 4 bits )
      net.WriteUInt(role, 2) -- ( 2 bits )
      net.WriteUInt(c4, bitsRequired(C4_WIRE_COUNT)) -- 0 -> 2^bits ( default c4: 3 bits )
      net.WriteUInt(dmg, 30) -- DMG_BUCKSHOT is the highest. ( 30 bits )
      net.WriteString(wep)
      net.WriteBool(hshot) -- ( 1 bit )
      net.WriteUInt(dtime, 15)
      net.WriteUInt(stime, 15)

      net.WriteUInt(#kill_entids, 8)
      for k, idx in ipairs(kill_entids) do
         net.WriteUInt(idx, plyBits)
      end

      net.WriteUInt(lastid, plyBits)

      -- Who found this, so if we get this from a detective we can decide not to
      -- show a window
      net.WriteUInt(ply:EntIndex(), plyBits)

      net.WriteString(words)

      -- 94 + string data + plyBits * (3 + #kill_entids)

   -- If found by detective, send to all, else just the finder
   if ply:IsActiveDetective() then
      net.Broadcast()
   else
      net.Send(ply)
   end
end


-- Returns a sample for use in dna scanner if the kill fits certain constraints,
-- else returns nil
local function GetKillerSample(victim, attacker, dmg)
   -- only guns and melee damage, not explosions
   if not (dmg:IsBulletDamage() or dmg:IsDamageType(DMG_SLASH) or dmg:IsDamageType(DMG_CLUB)) then
      return nil
   end

   if not (IsValid(victim) and IsValid(attacker) and attacker:IsPlayer()) then return end

   -- NPCs for which a player is damage owner (meaning despite the NPC dealing
   -- the damage, the attacker is a player) should not cause the player's DNA to
   -- end up on the corpse.
   local infl = dmg:GetInflictor()
   if IsValid(infl) and infl:IsNPC() then return end

   local dist = victim:GetPos():Distance(attacker:GetPos())
   if dist > GetConVar("ttt_killer_dna_range"):GetFloat() then return nil end

   local sample = {}
   sample.killer = attacker
   sample.killer_sid = attacker:SteamID() -- backwards compatibility; use sample.killer_sid64 instead
   sample.killer_sid64 = attacker:SteamID64()
   sample.victim = victim
   sample.t      = CurTime() + (-1 * (0.019 * dist)^2 + GetConVar("ttt_killer_dna_basetime"):GetFloat())

   return sample
end

local crimescene_keys = {"Fraction", "HitBox", "Normal", "HitPos", "StartPos"}
local poseparams = {
   "aim_yaw", "move_yaw", "aim_pitch",
--   "spine_yaw", "head_yaw", "head_pitch"
};

local function GetSceneDataFromPlayer(ply)
   local data = {
      pos      = ply:GetPos(),
      ang      = ply:GetAngles(),
      sequence = ply:GetSequence(),
      cycle    = ply:GetCycle()
   };

   for _, param in pairs(poseparams) do
      data[param] = ply:GetPoseParameter(param)
   end

   return data
end

local function GetSceneData(victim, attacker, dmginfo)
   -- only for guns for now, hull traces don't work well etc
   if not dmginfo:IsBulletDamage() then return end

   local scene = {}

   if victim.hit_trace then
      scene.hit_trace = table.CopyKeys(victim.hit_trace, crimescene_keys)
   else
      return scene
   end

   scene.victim = GetSceneDataFromPlayer(victim)

   if IsValid(attacker) and attacker:IsPlayer() then
      scene.killer = GetSceneDataFromPlayer(attacker)

      local att = attacker:LookupAttachment("anim_attachment_RH")
      local angpos = attacker:GetAttachment(att)
      if not angpos then
         scene.hit_trace.StartPos = attacker:GetShootPos()
      else
         scene.hit_trace.StartPos = angpos.Pos
      end
   end

   return scene
end

local rag_collide = CreateConVar("ttt_ragdoll_collide", "0")

-- Creates client or server ragdoll depending on settings
function CORPSE.Create(ply, attacker, dmginfo)
   if not IsValid(ply) then return end

   local efn = ply.effect_fn
   ply.effect_fn = nil

   local rag = ents.Create("prop_ragdoll")
   if not IsValid(rag) then return nil end

   rag:SetPos(ply:GetPos())
   rag:SetModel(ply:GetModel())
   rag:SetSkin(ply:GetSkin())
   for key, value in pairs(ply:GetBodyGroups()) do
      rag:SetBodygroup(value.id, ply:GetBodygroup(value.id))
   end
   rag:SetAngles(ply:GetAngles())
   rag:SetColor(ply:GetColor())

   rag:Spawn()
   rag:Activate()

   -- nonsolid to players, but can be picked up and shot
   rag:SetCollisionGroup(rag_collide:GetBool() and COLLISION_GROUP_WEAPON or COLLISION_GROUP_DEBRIS_TRIGGER)

   -- flag this ragdoll as being a player's
   rag.player_ragdoll = true
   rag.sid64 = ply:SteamID64()

   rag.sid = ply:SteamID() -- backwards compatibility; use rag.sid64 instead
   rag.uqid = ply:UniqueID() -- backwards compatibility; use rag.sid64 instead

   -- network data
   CORPSE.SetPlayerNick(rag, ply)
   CORPSE.SetFound(rag, false)
   CORPSE.SetCredits(rag, ply:GetCredits())

   -- if someone searches this body they can find info on the victim and the
   -- death circumstances
   rag.equipment = ply:GetEquipmentItems()
   rag.was_role = ply:GetRole()
   rag.bomb_wire = ply.bomb_wire
   rag.dmgtype = dmginfo:GetDamageType()

   local wep = util.WeaponFromDamage(dmginfo)
   rag.dmgwep = IsValid(wep) and wep:GetClass() or ""

   rag.was_headshot = (ply.was_headshot and dmginfo:IsBulletDamage())
   rag.time = CurTime()
   rag.kills = table.Copy(ply.kills)

   rag.killer_sample = GetKillerSample(ply, attacker, dmginfo)

   -- crime scene data
   rag.scene = GetSceneData(ply, attacker, dmginfo)


   -- position the bones
   local num = rag:GetPhysicsObjectCount()-1
   local v = ply:GetVelocity()

   -- bullets have a lot of force, which feels better when shooting props,
   -- but makes bodies fly, so dampen that here
   if dmginfo:IsDamageType(DMG_BULLET) or dmginfo:IsDamageType(DMG_SLASH) then
      v = v / 5
   end

   for i=0, num do
      local bone = rag:GetPhysicsObjectNum(i)
      if IsValid(bone) then
         local bp, ba = ply:GetBonePosition(rag:TranslatePhysBoneToBone(i))
         if bp and ba then
            bone:SetPos(bp)
            bone:SetAngles(ba)
         end

         -- not sure if this will work:
         bone:SetVelocity(v)
      end
   end

   -- create advanced death effects (knives)
   if efn then
      -- next frame, after physics is happy for this ragdoll
      timer.Simple(0, function() if IsValid(rag) then efn(rag) end end)
   end

   hook.Run("TTTOnCorpseCreated", rag, ply)

   return rag -- we'll be speccing this
end
