--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]


local PLUGIN = PLUGIN;

PLUGIN.name = "ArcCW Base";
PLUGIN.description = "Adds compatibility with ArcCW, integrating the weapons and attachments with helix items. Also links ArcCW SWEPs spread, recoil and sway to characters \"gun\" skill.";
PLUGIN.author = "Gr4Ss & LegAz";

if (!ArcCW) then return end

PLUGIN.attachmentTranslate = {}

for k, v in pairs(ArcCW.AttachmentTable) do
    local ITEM = ix.item.Register(k, nil, false, nil, true)
    ITEM.name = v.PrintName
    ITEM.category = "Attachment (ArcCW)"
    ITEM.model = "models/props_lab/box01a.mdl"
    ITEM.description = "A box containing a "..v.PrintName.." attachment."
end

ix.util.Include("sh_config.lua") -- moving config into another plugin just for visuals @offlegaz
ix.util.Include("sv_plugin.lua")
ix.util.Include("cl_plugin.lua")

ix.config.Add("forceUseMagazineSystem", true, "Should magazines be mandatory for item weapons, or is it OK to fall back to the Source ammo pool?", nil, {
	category = "general",
})

if (CLIENT) then
	function ArcCW:PlayerTakeAtt(ply, att, amt) end

	function ArcCW:PlayerGiveAtt(ply, att, amt) end
end

ix.config.Add("shootDbLevelReduction", 0, "How many DB to reduce the ARCCW sound levels by (this effects how far away it can be heard).", nil, {
	data = {min = 1, max = 50, decimals = 1},
	category = "general"
})

ix.config.Add("shootOtherDbLevelReduction", 50, "How many DB to reduce the ARCCW sound levels by (non gunshot sound effects).", nil, {
	data = {min = 1, max = 100, decimals = 1},
	category = "general"
})

ix.config.Add("shootOtherVolumeLevelReduction", 0.5, "How much to reduce the ARCCW sound levels by (non gunshot sound effects).", nil, {
	data = {min = 0, max = 1, decimals = 3},
	category = "general"
})

ix.config.Add("shootDbLevelReductionClose", 50, "How many DB to reduce the ARCCW sound levels by (this effects how far away close sounds can be heard).", nil, {
	data = {min = 1, max = 100, decimals = 1},
	category = "general"
})

ix.config.Add("shootVolumeReduction", 0, "How much to reduce the ARCCW sound levels by (this effects how far loud it is).", nil, {
	data = {min = 0, max = 1, decimals = 3},
	category = "general"
})

ix.config.Add("shootSoundDSP", 2, "Which DSP preset to use for close sounds (don't change this unless you know what you're doing!)", nil, {
	data = {min = 0, max = 140, decimals = 0},
	category = "general"
})

ix.config.Add("shootSoundDSPDistant", 31, "Which DSP preset to use for distant sounds (don't change this unless you know what you're doing!)", nil, {
	data = {min = 0, max = 140, decimals = 0},
	category = "general"
})

ix.config.Add("shotsBeforeBreakingWeapon", 900, "How many shots can single weapon handle without breaking", nil, {
	data = {min = 0, max = 9999},
	category = "general"
})

function PLUGIN:CanPlayerEquipItem(client, item)
	if (item.isWeapon and item:GetData("broken", false)) then
		return false
	end
end

--[[
	sound overrides for ArcCW
]]


local sndTbl = {} -- a table which we can use to queue up sounds asynchronously
local function networkAllPendingArcSounds()
	while true do
		for i, snd in ipairs(sndTbl) do
			if !snd.ent or !IsValid(snd.ent) then
				continue
			end

			if (!snd.filter) then
				snd.ent:EmitSound(snd.fsound, snd.lvl, 100, snd.vol, CHAN_STATIC, SND_NOFLAGS,
					snd.useDSP and snd.dsp or 0)
			else
				local _snd = CreateSound(snd.ent, snd.fsound, snd.filter)
				_snd:SetSoundLevel(snd.lvl)
				if (snd.useDSP) then
					_snd:SetDSP(snd.dsp or 0)
				end
				_snd:PlayEx(snd.vol, 255)
			end

			table.remove(sndTbl, i)
		end

		coroutine.yield()
	end
end

local function insertArcSound(ent, fsound, filter, lvl, vol, dsp, useDSP)
	sndTbl[#sndTbl + 1] = {
		ent = ent,
		fsound = fsound,
		filter = filter,
		lvl = lvl,
		vol = vol,
		dsp = dsp,
		useDSP = useDSP
	}
end

local sndCO = coroutine.create(networkAllPendingArcSounds)

if (SERVER) then
	timer.Create("ixNetworkArcCWSounds", 0.1, 0, function()
		-- gun sounds are kind of low-priority. improves S2K performance if we don't block the main thread with them
		if (!sndCO or !coroutine.resume(sndCO)) then
			-- there is something wrong with the table.Add
			ErrorNoHalt("ArcCW sound coroutine failed to resume. Attempting to recreate coroutine.\n")
			sndTbl = {}
			sndCO = coroutine.create(networkAllPendingArcSounds)
		end
	end)
end

local function isShootSound(ent, fsound)
	if (fsound == ent.ShootSound or fsound == ent.ShootSoundSilenced or fsound == ent.DistantShootSound) then
		return true
	end

	return false
end

timer.Simple(1, function()
	local tbl = weapons.GetStored("arccw_base") -- baller way of shoving our own shit code into arccw

	-- ^^ make sure we're the last to load so the override actually works
	function tbl:MyEmitSound(fsound, level, pitch, _vol, chan, useWorld)
		if !fsound then return end

		fsound = self:GetBuff_Hook("Hook_TranslateSound", fsound) or fsound
		local client = self:GetOwner()
		local weapon = client:GetActiveWeapon()
		local useDSP = isShootSound(weapon, fsound) -- don't play DSP on reloading and equipping sfx
		if istable(fsound) then fsound = self:TableRandom(fsound) end

		if fsound and fsound != "" then
			local lvl = math.Clamp(level or 150 - ix.config.Get("shootDbLevelReductionClose", 10), 60, 160)
			local vol = _vol or 1 - ix.config.Get("shootVolumeReduction", 0.1)
			local dsp = ix.config.Get("shootSoundDSP", 2)

			local lvl2 = math.Clamp(level or 150 - ix.config.Get("shootDbLevelReduction", 60), 60, 160)
			local vol2 = math.Clamp(_vol or 1 - ix.config.Get("shootVolumeReduction", 0.1) - 0.1, 0, 1)
			local dsp2 = ix.config.Get("shootSoundDSPDistant", 31)

			if (!useDSP) then
				lvl = math.Clamp(lvl - ix.config.Get("shootOtherDbLevelReduction", 50), 60, 160)
				vol = vol - ix.config.Get("shootOtherVolumeLevelReduction", 0.1)
			end

			if (CLIENT and client == LocalPlayer()) then
				-- client plays their own sound
				-- nevermind, we can't do this because i can't use CRecipientFilters to filter out the client serverside
				-- nevermind, apparently they don't duplicate any more ?????????????
				-- TODO: reimplement when that feature makes it to main gmod branch
				weapon:EmitSound(fsound, level, pitch, vol, chan or CHAN_STATIC)

			elseif (client and SERVER) then
				-- filter for distant sounding gunshots
				if (useDSP) then
					local filter = RecipientFilter()
					filter:AddAllPlayers()
					filter:RemovePlayer(client)
					filter:RemovePVS(client:GetPos())

					insertArcSound(weapon, fsound, filter, lvl2, vol2, dsp2, true)
				end

				weapon:EmitSound(fsound, lvl, 100, vol, CHAN_STATIC, SND_NOFLAGS, dsp)
			end
		end
	end
end)

function ArcCW:PlayerGetAtts(client, attachment)
    if (!IsValid(client)) then return 0 end
    if (GetConVar("arccw_attinv_free"):GetBool()) then return 999 end

    if (attachment == "") then return 999 end

    local attachmentData = ArcCW.AttachmentTable[attachment]
    if (!attachmentData) then return 0 end
    if (attachmentData.Free) then return 999 end

	local character = client:GetCharacter()
	if (!character) then return 0 end

    if (!client:IsAdmin() and attachmentData.AdminOnly) then
        return 0
    end

    if (attachmentData.InvAtt) then
		attachment = attachmentData.InvAtt
	end

	local weapon = client:GetActiveWeapon()
	if (SERVER and client.ixAttachAttempt and
		client.ixAttachAttempt.attachment == attachment and
		client.ixAttachAttempt.time + 1 > CurTime()) then
		weapon = client.ixAttachAttempt.weapon
	end

	local amount = character:GetInventory():GetItemCount(PLUGIN.attachmentTranslate[attachment] or attachment)
	for _, v in ipairs(weapon:GetNetVar("ixItemDefaultWeaponAtts", {})) do
		if (v == attachment) then amount = amount + 1 end
	end

	if (SERVER and weapon.ixItem) then
		for _, v in pairs(weapon.ixItem:GetData("WeaponAttachments", {})) do
			if (v.attachment == attachment) then
				amount = amount + 1
				break
			end
		end
	elseif (CLIENT and weapon:GetNetVar("ixItemID")) then
		local item = ix.item.instances[weapon:GetNetVar("ixItemID")]
		if (item) then
			for _, v in pairs(item:GetData("WeaponAttachments", {})) do
				if (v.attachment == attachment) then
					amount = amount + 1
					break
				end
			end
		end
	end

    return amount
end

function PLUGIN:ArcCW_PlayerCanAttach(client, weapon, attachment, slot, detach)
	client.ixAttachAttempt = nil
	client.ixDeattachAttempt = nil

	if (detach) then
		client.ixDeattachAttempt = {
			weapon = weapon,
			attachment = attachment,
			slot = slot
		}
		return
	end
	if (attachment == "") then return end

	local weapon2 = client.ixAttachWeapon or client:GetActiveWeapon()
	if (weapon2 != weapon) then return end

	client.ixAttachAttempt = {
		weapon = weapon,
		attachment = attachment,
		slot = slot,
		time = CurTime()
	}
	for _, v in ipairs(weapon:GetNetVar("ixItemDefaultWeaponAtts", {})) do
		if (v == attachment) then
			client.ixAttachAttempt.mode = "default"
			return true
		end
	end

	if (weapon.ixItem) then
		for _, v in pairs(weapon.ixItem:GetData("WeaponAttachments", {})) do
			if (v.attachment == attachment) then
				client.ixAttachAttempt.mode = "reattach"
				return true
			end
		end
	end

	local character = client:GetCharacter()
	if (character) then
		local inventory = character:GetInventory()
		if (self.attachmentTranslate[attachment]) then
			if (!inventory:HasItem(self.attachmentTranslate[attachment])) then
				return false
			end
		elseif (!inventory:HasItem(attachment)) then
			return false
		end
	end

	client.ixAttachAttempt.mode = "item"
	return true
end

-- Give one ammo otherwise ArcCW doesn't let us reload
function PLUGIN:KeyPress(client, key)
	if (key == IN_RELOAD) then
		local weapon = client:GetActiveWeapon()
		if (IsValid(weapon) and	weapon:GetNetVar("ixItemID") and client:GetAmmoCount(weapon:GetPrimaryAmmoType()) == 0) then
			if (client.ixPreReloadAmmoGiven) then
				-- We still have 1 ammo from some previous attempt, lets remove this first!
				client:SetAmmo(math.max(client:GetAmmoCount(client.ixPreReloadAmmoGiven) - 1, 0), client.ixPreReloadAmmoGiven)
			end
			client:SetAmmo(1, weapon:GetPrimaryAmmoType())
			client.ixPreReloadAmmoGiven = weapon:GetPrimaryAmmoType()
		end
	end
end

-- Take the ammo again if it wasn't used
function PLUGIN:KeyRelease(client, key)
	if (client.ixPreReloadAmmoGiven) then
		client:SetAmmo(client:GetAmmoCount(client.ixPreReloadAmmoGiven) - 1, client.ixPreReloadAmmoGiven)
		client.ixPreReloadAmmoGiven = nil
	end
end

function PLUGIN:PlayerSwitchWeapon(client, oldWeapon, newWeapon)
	if (client.ixPreReloadAmmoGiven) then
		client:SetAmmo(client:GetAmmoCount(client.ixPreReloadAmmoGiven) - 1, client.ixPreReloadAmmoGiven)
		client.ixPreReloadAmmoGiven = nil
	end
end

local sortFunc = function(a, b)
	local aAmmo, bAmmo = a:GetAmmo(), b:GetAmmo()
	if (aAmmo != bAmmo) then
		return aAmmo > bAmmo
	else
		return a:GetID() > b:GetID()
	end
end

function PLUGIN:Hook_PreReload(weapon)
	local client = weapon:GetOwner()
	if (!IsValid(client) or client:IsNPC()) then return end

	-- Take the 1 ammo given to let us get past the 'not zero ammo' check that happens before this hook
	if (client.ixPreReloadAmmoGiven) then
		client:SetAmmo(client:GetAmmoCount(client.ixPreReloadAmmoGiven) - 1, client.ixPreReloadAmmoGiven)
		client.ixPreReloadAmmoGiven = nil
	end

	local character = client:GetCharacter()
	if (!character) then return end

	if (weapon:GetNetVar("ixItemID") == nil) then return end

	local itemID = weapon:GetNetVar("ixItemID")
	local item = ix.item.instances[itemID]
	if (!item) then return end

	local ammoID = weapon:GetPrimaryAmmoType()
	local ammoName = string.lower(game.GetAmmoName(ammoID) or "")
	if (ammoName == "") then return end

	local ammoItems = {}
	for k, v in ipairs(character:GetInventory():GetItemsByBase("base_arccwmag")) do
		if (item.magazines and item.magazines[v.uniqueID] and v:GetAmmo() > 0) then
			ammoItems[#ammoItems + 1] = v
		end
	end

	table.sort(ammoItems, sortFunc)

	local ammoItem = ammoItems[1]
	if (!ammoItem) then return ix.config.Get("forceUseMagazineSystem") end

	local chamber = math.Clamp(weapon:Clip1(), 0, weapon:GetChamberSize())
	if (SERVER) then
		if (weapon.ixItem:GetData("reloadMagazineItem")) then
			self:RefundAmmoItem(weapon, character, weapon.ixItem:GetData("reloadMagazineItem"), weapon:Clip1() + client:GetAmmoCount(ammoID) - chamber, weapon.ixItem:GetData("reloadMagazineInvPos"))
			weapon:SetClip1(chamber)
		end
		weapon.ixItem:SetData("reloadMagazineItem", ammoItem:GetID())
		weapon.ixItem:SetData("reloadMagazineInvPos", {ammoItem.invID, ammoItem.gridX, ammoItem.gridY})
		ammoItem:Transfer(nil, nil, nil, client, nil, true)
	end

	client:SetAmmo(ammoItem:GetAmmo(), ammoID)
end

function PLUGIN:Hook_ModDispersion(weapon, hip)
	local weaponOwner = weapon:GetOwner()

	if (weaponOwner:IsPlayer()) then
		weaponOwner = weaponOwner:GetCharacter()
	else
		return
	end

	local skillLevel = weaponOwner:GetSkillLevel("guns")
	local minSkillLevel, maxSkillLevel = ix.weapons:GetWeaponSkillRequired(weapon:GetClass())

	if (skillLevel < minSkillLevel) then
		return hip + math.ceil(hip * self.spreadMaxIncrease) * (1 - skillLevel / minSkillLevel)
	end

	skillLevel = math.min(skillLevel, maxSkillLevel)
	return hip - math.floor(hip * self.spreadMaxDecrease) * ((skillLevel - minSkillLevel) / (maxSkillLevel - minSkillLevel))
end

function PLUGIN:Hook_ModifyRecoil(weapon, rec)
	local weaponOwner = weapon:GetOwner()

	if (weaponOwner:IsPlayer()) then
		weaponOwner = weaponOwner:GetCharacter()
	else
		return
	end

	local skillLevel = weaponOwner:GetSkillLevel("guns")
	local minSkillLevel, maxSkillLevel = ix.weapons:GetWeaponSkillRequired(weapon:GetClass())
	local modifier

	if (skillLevel < minSkillLevel) then
		modifier = PLUGIN.recoilMaxIncrease * (1 - skillLevel / minSkillLevel)
	else
		skillLevel = math.min(skillLevel, maxSkillLevel)
		modifier = -(PLUGIN.recoilMaxDecrease * ((skillLevel - minSkillLevel) / (maxSkillLevel - minSkillLevel)))
	end

	rec.Recoil = rec.Recoil + (rec.Recoil * modifier)
	rec.RecoilSide = rec.RecoilSide + (rec.RecoilSide * modifier)
	-- rec.VisualRecoilMul = rec.VisualRecoilMul + (rec.VisualRecoilMul * modifier)

	return rec
end

function PLUGIN:O_Hook_Override_CanBash()
	return false
end
