
include("weaponry_shd.lua") -- inits WEPS tbl

---- Weapon system, pickup limits, etc

local IsEquipment = WEPS.IsEquipment

-- Prevent players from picking up multiple weapons of the same type etc
function GM:PlayerCanPickupWeapon(ply, wep)
   if not IsValid(wep) or not IsValid(ply) then return end
   if ply:IsSpec() then return false end

   -- While resetting the map, players should not be allowed to pick up the newly-reset weapon
   -- entities, because they would be stripped again during the player spawning process and
   -- subsequently be missing.
   if GAMEMODE.RespawningWeapons then
      return false
   end

   -- Disallow picking up for ammo
   if ply:HasWeapon(wep:GetClass()) then
      return false
   elseif not ply:CanCarryWeapon(wep) then
      return false
   elseif IsEquipment(wep) and wep.IsDropped and (not ply:KeyDown(IN_USE)) then
      return false
   end

   local tr = util.TraceEntity({start=wep:GetPos(), endpos=ply:GetShootPos(), mask=MASK_SOLID}, wep)
   if tr.Fraction == 1.0 or tr.Entity == ply then
      wep:SetPos(ply:GetShootPos())
   end

   return true
end

-- Cache role -> default-weapons table
local loadout_weapons = nil
local function GetLoadoutWeapons(r)
   if not loadout_weapons then
      local tbl = {
         [ROLE_INNOCENT] = {},
         [ROLE_TRAITOR]  = {},
         [ROLE_DETECTIVE]= {}
      };

      for k, w in pairs(weapons.GetList()) do
         if w and istable(w.InLoadoutFor) then
            for _, wrole in pairs(w.InLoadoutFor) do
               table.insert(tbl[wrole], WEPS.GetClass(w))
            end
         end
      end

      loadout_weapons = tbl
   end

   return loadout_weapons[r]
end

-- Give player loadout weapons he should have for his role that he does not have
-- yet
local function GiveLoadoutWeapons(ply)
   local r = GetRoundState() == ROUND_PREP and ROLE_INNOCENT or ply:GetRole()
   local weps = GetLoadoutWeapons(r)
   if not weps then return end

   for _, cls in pairs(weps) do
      if not ply:HasWeapon(cls) and ply:CanCarryType(WEPS.TypeForWeapon(cls)) then
         ply:Give(cls)
      end
   end
end

local function HasLoadoutWeapons(ply)
   if ply:IsSpec() then return true end

   local r = GetRoundState() == ROUND_PREP and ROLE_INNOCENT or ply:GetRole()
   local weps = GetLoadoutWeapons(r)
   if not weps then return true end


   for _, cls in pairs(weps) do
      if not ply:HasWeapon(cls) and ply:CanCarryType(WEPS.TypeForWeapon(cls)) then
         return false
      end
   end

   return true
end

-- Give loadout items.
local function GiveLoadoutItems(ply)
   local items = EquipmentItems[ply:GetRole()]
   if items then
      for _, item in pairs(items) do
         if item.loadout and item.id then
            ply:GiveEquipmentItem(item.id)
         end
      end
   end
end

-- Quick hack to limit hats to models that fit them well
local Hattables = { "phoenix.mdl", "arctic.mdl", "Group01", "monk.mdl" }
local function CanWearHat(ply)
   local path = string.Explode("/", ply:GetModel())
   if #path == 1 then path = string.Explode("\\", path) end

   return table.HasValue(Hattables, path[3])
end

CreateConVar("ttt_detective_hats", "1")
-- Just hats right now
local function GiveLoadoutSpecial(ply)
   if ply:IsActiveDetective() and GetConVar("ttt_detective_hats"):GetBool() and CanWearHat(ply) then

      if not IsValid(ply.hat) then
         local hat = ents.Create("ttt_hat_deerstalker")
         if not IsValid(hat) then return end

         hat:SetPos(ply:GetPos() + Vector(0,0,70))
         hat:SetAngles(ply:GetAngles())

         hat:SetParent(ply)

         ply.hat = hat

         hat:Spawn()
      end
   else
      SafeRemoveEntity(ply.hat)

      ply.hat = nil
   end
end

-- Sometimes, in cramped map locations, giving players weapons fails. A timer
-- calling this function is used to get them the weapons anyway as soon as
-- possible.
local function LateLoadout(id)
   local ply = Entity(id)
   if not IsValid(ply) or not ply:IsPlayer() then
      timer.Remove("lateloadout" .. id)
      return
   end

   if not HasLoadoutWeapons(ply) then
      GiveLoadoutWeapons(ply)

      if HasLoadoutWeapons(ply) then
         timer.Remove("lateloadout" .. id)
      end
   end
end

-- Note that this is called both when a player spawns and when a round starts
function GM:PlayerLoadout( ply )
   if IsValid(ply) and (not ply:IsSpec()) then
      -- clear out equipment flags
      ply:ResetEquipment()

      -- give default items
      GiveLoadoutItems(ply)

      -- hand out weaponry
      GiveLoadoutWeapons(ply)

      GiveLoadoutSpecial(ply)

      if not HasLoadoutWeapons(ply) then
         MsgN("Could not spawn all loadout weapons for " .. ply:Nick() .. ", will retry.")
         timer.Create("lateloadout" .. ply:EntIndex(), 1, 0,
                      function() LateLoadout(ply:EntIndex()) end)
      end
   end
end

function GM:UpdatePlayerLoadouts()
   for _, ply in player.Iterator() do
      hook.Call("PlayerLoadout", GAMEMODE, ply)
   end
end

---- Weapon dropping

function WEPS.DropNotifiedWeapon(ply, wep, death_drop)
   if IsValid(ply) and IsValid(wep) then
      -- Hack to tell the weapon it's about to be dropped and should do what it
      -- must right now
      if wep.PreDrop then
         wep:PreDrop(death_drop)
      end

      -- PreDrop might destroy weapon
      if not IsValid(wep) then return end

      -- Tag this weapon as dropped, so that if it's a special weapon we do not
      -- auto-pickup when nearby.
      wep.IsDropped = true

      -- After dropping a weapon, always switch to holstered, so that traitors
      -- will never accidentally pull out a traitor weapon.
      --
      -- Perform this *before* the drop in order to abuse the fact that this
      -- holsters the weapon, which in turn aborts any reload that's in
      -- progress. We don't want a dropped weapon to be in a reloading state
      -- because the relevant timer is reset when picking it up, making the
      -- reload happen instantly. This allows one to dodge the delay by dropping
      -- during reload. All of this is a workaround for not having access to
      -- CBaseWeapon::AbortReload() (and that not being handled in
      -- CBaseWeapon::Drop in the first place).
      ply:SelectWeapon("weapon_ttt_unarmed")

      ply:DropWeapon(wep)

      wep:PhysWake()
   end
end

local function DropActiveWeapon(ply)
   if not IsValid(ply) then return end

   local wep = ply:GetActiveWeapon()

   if not IsValid(wep) then return end

   if wep.AllowDrop == false then
      return
   end

   local tr = util.QuickTrace(ply:GetShootPos(), ply:GetAimVector() * 32, ply)

   if tr.HitWorld then
      LANG.Msg(ply, "drop_no_room")
      return
   end

   ply:AnimPerformGesture(ACT_GMOD_GESTURE_ITEM_PLACE)

   WEPS.DropNotifiedWeapon(ply, wep)
end
concommand.Add("ttt_dropweapon", DropActiveWeapon)

local function DropActiveAmmo(ply)
   if not IsValid(ply) then return end

   local wep = ply:GetActiveWeapon()
   if not IsValid(wep) then return end

   if not wep.AmmoEnt then return end

   local amt = wep:Clip1()
   if amt < 1 or amt <= (wep.Primary.ClipSize * 0.25) then
      LANG.Msg(ply, "drop_no_ammo")
      return
   end

   local pos, ang = ply:GetShootPos(), ply:EyeAngles()
   local dir = (ang:Forward() * 32) + (ang:Right() * 6) + (ang:Up() * -5)

   local tr = util.QuickTrace(pos, dir, ply)
   if tr.HitWorld then return end

   wep:SetClip1(0)

   ply:AnimPerformGesture(ACT_GMOD_GESTURE_ITEM_GIVE)

   local box = ents.Create(wep.AmmoEnt)
   if not IsValid(box) then return end

   box:SetPos(pos + dir)
   box:SetOwner(ply)
   box:Spawn()

   box:PhysWake()

   local phys = box:GetPhysicsObject()
   if IsValid(phys) then
      phys:ApplyForceCenter(ang:Forward() * 1000)
      phys:ApplyForceOffset(VectorRand(), vector_origin)
   end

   box.AmmoAmount = amt

   timer.Simple(2, function()
                      if IsValid(box) then
                         box:SetOwner(nil)
                      end
                   end)
end
concommand.Add("ttt_dropammo", DropActiveAmmo)


-- Give a weapon to a player. If the initial attempt fails due to heisenbugs in
-- the map, keep trying until the player has moved to a better spot where it
-- does work.
local function GiveEquipmentWeapon(sid64, cls)
   -- Referring to players by SteamID64 because a player may disconnect while his
   -- unique timer still runs, in which case we want to be able to stop it. For
   -- that we need its name, and hence his SteamID64.
   local ply = player.GetBySteamID64(sid64)
   local tmr = "give_equipment" .. sid64

   if (not IsValid(ply)) or (not ply:IsActiveSpecial()) then
      timer.Remove(tmr)
      return
   end

   -- giving attempt, will fail if we're in a crazy spot in the map or perhaps
   -- other glitchy cases
   local w = ply:Give(cls)

   if (not IsValid(w)) or (not ply:HasWeapon(cls)) then
      if not timer.Exists(tmr) then
         timer.Create(tmr, 1, 0, function() GiveEquipmentWeapon(sid64, cls) end)
      end

      -- we will be retrying
   else
      -- can stop retrying, if we were
      timer.Remove(tmr)

      if w.WasBought then
         -- some weapons give extra ammo after being bought, etc
         w:WasBought(ply)
      end
   end
end

local function HasPendingOrder(ply)
   return timer.Exists("give_equipment" .. tostring(ply:SteamID64()))
end

function GM:TTTCanOrderEquipment(ply, id, is_item)
   --- return true to allow buying of an equipment item, false to disallow
   return true
end

local bitsRequired = util.BitsRequired

-- Equipment buying
local function OrderEquipment(ply, cmd, args)
   if not IsValid(ply) or #args != 1 then return end

   if not (ply:IsActiveTraitor() or ply:IsActiveDetective()) then return end

   -- no credits, can't happen when buying through menu as button will be off
   if ply:GetCredits() < 1 then return end

   -- it's an item if the arg is an id instead of an ent name
   local id = args[1]
   local is_item = tonumber(id)

   if not hook.Run("TTTCanOrderEquipment", ply, id, is_item) then return end

   -- we use weapons.GetStored to save time on an unnecessary copy, we will not
   -- be modifying it
   local swep_table = (not is_item) and weapons.GetStored(id) or nil

   -- some weapons can only be bought once per player per round, this used to be
   -- defined in a table here, but is now in the SWEP's table
   if swep_table and swep_table.LimitedStock and ply:HasBought(id) then
      LANG.Msg(ply, "buy_no_stock")
      return
   end

   local received = false

   if is_item then
      id = tonumber(id)

      -- item whitelist check
      local allowed = GetEquipmentItem(ply:GetRole(), id)

      if not allowed then
         print(ply, "tried to buy item not buyable for his class:", id)
         return
      end

      -- ownership check and finalise
      if id and EQUIP_NONE < id then
         if not ply:HasEquipmentItem(id) then
            ply:GiveEquipmentItem(id)
            received = true
         end
      end
   elseif swep_table then
      -- weapon whitelist check
      if not table.HasValue(swep_table.CanBuy, ply:GetRole()) then
         print(ply, "tried to buy weapon his role is not permitted to buy")
         return
      end

      -- if we have a pending order because we are in a confined space, don't
      -- start a new one
      if HasPendingOrder(ply) then
         LANG.Msg(ply, "buy_pending")
         return
      end

      -- no longer restricted to only WEAPON_EQUIP weapons, just anything that
      -- is whitelisted and carryable
      if ply:CanCarryWeapon(swep_table) then
         GiveEquipmentWeapon(ply:SteamID64(), id)

         received = true
      end
   end

   if received then
      ply:SubtractCredits(1)
      LANG.Msg(ply, "buy_received")

      ply:AddBought(id)

      timer.Simple(0.5,
                   function()
                      if not IsValid(ply) then return end
                      net.Start("TTT_BoughtItem")
                      net.WriteBool(is_item)
                      if is_item then
                         -- since we only network one item here, convert the bitflag equipment id
                         -- into a power of two exponent before networking it

                         local exponent = math.log(id) / math.log(2)
                         net.WriteUInt(exponent, bitsRequired(math.log(EQUIP_MAX) / math.log(2)))
                      else
                         net.WriteString(id)
                      end
                      net.Send(ply)
                   end)

      hook.Call("TTTOrderedEquipment", GAMEMODE, ply, id, is_item)
   end
end
concommand.Add("ttt_order_equipment", OrderEquipment)

function GM:TTTToggleDisguiser(ply, state)
   -- Can be used to prevent players from using this button.
   -- return true to prevent it.
end

local function SetDisguise(ply, cmd, args)
   if not IsValid(ply) or not ply:IsActiveTraitor() then return end

   if ply:HasEquipmentItem(EQUIP_DISGUISE) then
      local state = #args == 1 and tobool(args[1])
      if hook.Run("TTTToggleDisguiser", ply, state) then return end

      ply:SetNWBool("disguised", state)
      LANG.Msg(ply, state and "disg_turned_on" or "disg_turned_off")
   end
end
concommand.Add("ttt_set_disguise", SetDisguise)

local function CheatCredits(ply)
   if IsValid(ply) then
      ply:AddCredits(10)
   end
end
concommand.Add("ttt_cheat_credits", CheatCredits, nil, nil, FCVAR_CHEAT)

local function TransferCredits(ply, cmd, args)
   if (not IsValid(ply)) or (not ply:IsActiveSpecial()) then return end
   if #args != 2 then return end

   local sid64 = tostring(args[1])
   local credits = tonumber(args[2])
   if sid64 and credits then
      local target = player.GetBySteamID64(sid64)
      if (not IsValid(target)) or (not target:IsActiveSpecial()) or (target:GetRole() ~= ply:GetRole()) or (target == ply) then
         LANG.Msg(ply, "xfer_no_recip")
         return
      end

      if ply:GetCredits() < credits then
         LANG.Msg(ply, "xfer_no_credits")
         return
      end

      credits = math.Clamp(credits, 0, ply:GetCredits())
      if credits == 0 then return end

      ply:SubtractCredits(credits)
      target:AddCredits(credits)

      LANG.Msg(ply, "xfer_success", {player=target:Nick()})
      LANG.Msg(target, "xfer_received", {player = ply:Nick(), num = credits})
   end
end
concommand.Add("ttt_transfer_credits", TransferCredits)

-- Protect against non-TTT weapons that may break the HUD
function GM:WeaponEquip(wep)
   if IsValid(wep) then
      -- only remove if they lack critical stuff
      if not wep.Kind then
         wep:Remove()
         ErrorNoHalt("Equipped weapon " .. wep:GetClass() .. " is not compatible with TTT\n")
      end
   end
end

-- non-cheat developer commands can reveal precaching the first time equipment
-- is bought, so trigger it at the start of a round instead
function WEPS.ForcePrecache()
   for k, w in ipairs(weapons.GetList()) do
      if w.WorldModel then
         util.PrecacheModel(w.WorldModel)
      end
      if w.ViewModel then
         util.PrecacheModel(w.ViewModel)
      end
   end
end
