local lshift = bit.lshift
local band = bit.band
local bor = bit.bor

local sp = game.SinglePlayer()
local l_CT = CurTime

local is, spr, wlk, cst

--[[
Function Name:  ResetEvents
Syntax: self:ResetEvents()
Returns:  Nothing.
Purpose:  Cleans up events table.
]]--
function SWEP:ResetEvents()
	self:SetEventStatus1(0x00000000)
	self:SetEventStatus2(0x00000000)
	self:SetEventStatus3(0x00000000)
	self:SetEventStatus4(0x00000000)
	self:SetEventStatus5(0x00000000)
	self:SetEventStatus6(0x00000000)
	self:SetEventStatus7(0x00000000)
	self:SetEventStatus8(0x00000000)

	self:SetEventTimer(l_CT())
	-- self:SetFirstDeployEvent(false)

	if self.EventTable then
		for _, eventtable in pairs(self.EventTable) do
			for i = 1, #eventtable do
				eventtable[i].called = false
			end
		end
	end

	if self.event_table_overflow then
		local editcts = self.EventTableEdict

		if editcts[0] then
			editcts[0].called = false

			for i = 1, #editcts do
				editcts[i].called = false
			end
		end
	end

	if sp then
		self:CallOnClient("ResetEvents", "")
	end
end

function SWEP:GetEventPlayed(event_slot)
	if self.event_table_overflow then
		return assert(self.EventTableEdict[event_slot], string.format("Unknown event %d", event_slot)).called
	end

	local inner_index = event_slot % 32
	local outer_index = (event_slot - inner_index) / 32 + 1
	local lindex = lshift(1, inner_index)
	return band(self.get_event_status_lut[outer_index](self), lindex) ~= 0, inner_index, outer_index, lindex
end

function SWEP:SetEventPlayed(event_slot)
	if self.event_table_overflow then
		assert(self.EventTableEdict[event_slot], string.format("Unknown event %d", event_slot)).called = true
		return
	end

	local inner_index = event_slot % 32
	local outer_index = (event_slot - inner_index) / 32 + 1
	local lindex = lshift(1, inner_index)

	self.set_event_status_lut[outer_index](self, bor(self.get_event_status_lut[outer_index](self), lindex))
	return inner_index, outer_index, lindex
end

--[[
Function Name:  ProcessEvents
Syntax: self:ProcessEvents().
Returns:  Nothing.
Notes: Critical for the event table to function.
Purpose:  Main SWEP function
]]--

SWEP._EventSlotCount = 0
SWEP.EventTableEdict = {}

function SWEP:DispatchLuaEvent(arg)
	if not self.event_table_built then
		self:RebuildEventEdictTable()
	end

	local fn = assert(assert(self.EventTableEdict[tonumber(arg)], "No such event with edict " .. arg).value, "Event is missing a function to call")
	assert(isfunction(fn), "Event " .. arg .. " is not a Lua event")
	fn(self, self:VMIV(), true)
end

function SWEP:DispatchBodygroupEvent(arg)
	if not self.event_table_built then
		self:RebuildEventEdictTable()
	end

	local event = assert(self.EventTableEdict[tonumber(arg)], "No such event with edict " .. arg)
	assert(isstring(event.name), "Event " .. arg .. " is missing bodygroup name to set")
	assert(isstring(event.value), "Event " .. arg .. " is missing bodygroup value to set")

	if event.view then
		self.ViewModelBodygroups[event.name] = event.value
	end

	if event.world then
		self.WorldModelBodygroups[event.name] = event.value
	end
end

local isstring = isstring

local function eventtablesorter(a, b)
	local sa, sb = isstring(a), isstring(b)

	if sa and not sb or not sa and sb then
		if sa then
			return false
		end

		return true
	end

	return a < b
end

function SWEP:RebuildEventEdictTable()
	local self2 = self:GetTable()
	local slot = 0

	for i = #self2.EventTableEdict, 0, -1 do
		self2.EventTableEdict[i] = nil
	end

	self:ResetEvents()

	local eventtable = self2.EventTable
	eventtable.BaseClass = nil

	local keys = table.GetKeys(eventtable)
	table.sort(keys, eventtablesorter)

	for _, key in ipairs(keys) do
		local value = eventtable[key]

		if istable(value) then
			for _, event in SortedPairs(value) do
				if istable(event) then
					event.slot = slot
					slot = slot + 1

					if not event.autodetect then
						if event.type == "lua" then
							if event.server == nil then
								event.server = true
							end
						elseif event.type == "snd" or event.type == "sound" then
							if event.server == nil then
								event.server = false
							end
						elseif event.type == "bg" or event.type == "bodygroup" then
							if event.server == nil then event.server = true end
							if event.view == nil then event.view = true end
							if event.world == nil then event.world = true end
						end

						if event.client == nil then
							event.client = true
						end

						event.autodetect = true
					end

					event.called = false

					if slot > 256 and not self.event_table_warning then
						ErrorNoHalt("[TFA Base] Weapon " .. self:GetClass() .. " got too many events! 256 is maximum! Event table would NOT be properly predicted this time!\n")
						self.event_table_warning = true
					end

					self2.EventTableEdict[event.slot] = event
				end
			end
		end
	end

	self.event_table_overflow = slot > 256
	self._built_event_debug_string_fn = nil

	self._EventSlotCount = math.ceil(slot / 32)
	self._EventSlotNum = slot - 1
	self.event_table_built = true
end

function SWEP:ProcessEvents(firstprediction)
	local viewmodel = self:VMIVNPC()
	if not viewmodel then return end

	if not self.event_table_built then
		self:RebuildEventEdictTable()
	end

	if sp and CLIENT then return end
	if sp and SERVER then return self:ProcessEventsSP() end

	local ply = self:GetOwner()
	local isplayer = ply:IsPlayer()

	local evtbl = self.EventTable[self:GetLastActivity() or -1] or self.EventTable[viewmodel:GetSequenceName(viewmodel:GetSequence())]
	if not evtbl then return end

	local curtime = l_CT()
	local eventtimer = self:GetEventTimer()
	local is_local = CLIENT and ply == LocalPlayer()
	local animrate = self:GetAnimationRate(self:GetLastActivity() or -1)

	self.current_event_iftp = firstprediction
	self.processing_events = true

	for i = 1, #evtbl do
		local event = evtbl[i]
		if self:GetEventPlayed(event.slot) or curtime < eventtimer + event.time / animrate then goto CONTINUE end
		self:SetEventPlayed(event.slot)
		event.called = true

		if not event.autodetect then
			if event.type == "lua" then
				if event.server == nil then
					event.server = true
				end
			elseif event.type == "snd" or event.type == "sound" then
				if event.server == nil then
					event.server = false
				end
			elseif event.type == "bg" or event.type == "bodygroup" then
				if event.server == nil then event.server = true end
				if event.view == nil then event.view = true end
				if event.world == nil then event.world = true end
			end

			if event.client == nil then
				event.client = true
			end

			event.autodetect = true
		end

		if event.type == "lua" then
			if ((event.client and CLIENT and (not event.client_predictedonly or is_local)) or (event.server and SERVER)) and event.value then
				event.value(self, viewmodel, firstprediction)
			end
		elseif event.type == "snd" or event.type == "sound" then
			if SERVER then
				if event.client then
					if not isplayer and player.GetCount() ~= 0 then
						net.Start("tfaSoundEvent", true)
						net.WriteEntity(self)
						net.WriteString(event.value or "")
						net.WriteBool(event.shouldpause or false)
						net.SendPVS(self:GetPos())
					elseif isplayer then
						net.Start("tfaSoundEvent", true)
						net.WriteEntity(self)
						net.WriteString(event.value or "")
						net.WriteBool(event.shouldpause or false)
						net.SendOmit(ply)
					end
				elseif event.server and event.value and event.value ~= "" then
					self:EmitSound(event.value)
				end
			elseif event.client and is_local and not sp and event.value and event.value ~= "" then
				if firstprediction or firstprediction == nil then
					if event.time <= 0.01 then
						self:EmitSoundSafe(event.value)
					else
						self:EmitSound(event.value)
					end
				end
			end
		elseif event.type == "bg" or event.type == "bodygroup" then
			if ((event.client and CLIENT and (not event.client_predictedonly or is_local)) or
				(event.server and SERVER)) and (event.name and event.value and event.value ~= "") then

				if event.view then
					self.ViewModelBodygroups[event.name] = event.value
				end

				if event.world then
					self.WorldModelBodygroups[event.name] = event.value
				end
			end
		end

		::CONTINUE::
	end

	self.processing_events = false
	self.current_event_iftp = nil
end

-- This function is exclusively targeting singleplayer
function SWEP:ProcessEventsSP(firstprediction)
	local viewmodel = self:VMIVNPC()
	if not viewmodel then return end

	local evtbl = self.EventTable[self:GetLastActivity() or -1] or self.EventTable[viewmodel:GetSequenceName(viewmodel:GetSequence())]
	if not evtbl then return end

	local curtime = l_CT()
	local eventtimer = self:GetEventTimer()
	local is_local = self:GetOwner() == Entity(1)
	local animrate = self:GetAnimationRate(self:GetLastActivity() or -1)

	self.processing_events = true

	for i = 1, #evtbl do
		local event = evtbl[i]
		if self:GetEventPlayed(event.slot) or curtime < eventtimer + event.time / animrate then goto CONTINUE end
		self:SetEventPlayed(event.slot)
		event.called = true

		if not event.autodetect then
			if event.type == "lua" then
				if event.server == nil then
					event.server = true
				end
			elseif event.type == "snd" or event.type == "sound" then
				if event.server == nil then
					event.server = false
				end
			elseif event.type == "bg" or event.type == "bodygroup" then
				if event.server == nil then event.server = true end
				if event.view == nil then event.view = true end
				if event.world == nil then event.world = true end
			end

			if event.client == nil then
				event.client = true
			end

			event.autodetect = true
		end

		if event.type == "lua" then
			if event.value then
				if event.server then
					event.value(self, viewmodel, true)
				end

				if event.client and (not event.client_predictedonly or is_local) then
					self:CallOnClient("DispatchLuaEvent", tostring(event.slot))
				end
			end
		elseif event.type == "snd" or event.type == "sound" then
			if event.client then
				net.Start("tfaSoundEvent", true)
				net.WriteEntity(self)
				net.WriteString(event.value or "")
				net.WriteBool(event.shouldpause or false)
				net.Broadcast()
			elseif event.server and event.value and event.value ~= "" then
				self:EmitSound(event.value)
			end
		elseif event.type == "bg" or event.type == "bodygroup" then
			if event.name and event.value and event.value ~= "" then
				if event.server then
					if event.view then
						self.ViewModelBodygroups[event.name] = event.value
					end

					if event.world then
						self.WorldModelBodygroups[event.name] = event.value
					end
				end

				if event.client and (not event.client_predictedonly or is_local) then
					self:CallOnClient("DispatchBodygroupEvent", tostring(event.slot))
				end
			end
		end

		::CONTINUE::
	end

	self.processing_events = false
end

function SWEP:EmitSoundSafe(snd)
	timer.Simple(0, function()
		if IsValid(self) and snd then self:EmitSound(snd) end
	end)
end

local ct, stat, statend, finalstat, waittime, lact

function SWEP:ProcessStatus()
	local self2 = self:GetTable()

	is = self2.GetIronSightsRaw(self)
	spr = self2.GetSprinting(self)
	wlk = self2.GetWalking(self)
	cst = self2.GetCustomizing(self)

	local ply = self:GetOwner()
	local isplayer = ply:IsPlayer()

	if stat == TFA.Enum.STATUS_FIDGET and is then
		self:SetStatusEnd(0)

		self2.Idle_Mode_Old = self2.Idle_Mode
		self2.Idle_Mode = TFA.Enum.IDLE_BOTH
		self2.ClearStatCache(self, "Idle_Mode")
		self2.ChooseIdleAnim(self)

		if sp then
			self:CallOnClient("ChooseIdleAnim", "")
		end

		self2.Idle_Mode = self2.Idle_Mode_Old
		self2.ClearStatCache(self, "Idle_Mode")
		self2.Idle_Mode_Old = nil
		statend = -1
	end

	is = self:GetIronSights()
	stat = self:GetStatus()
	statend = self:GetStatusEnd()

	ct = l_CT()

	if stat ~= TFA.Enum.STATUS_IDLE and ct > statend then
		self:SetFirstDeployEvent(false)
		finalstat = TFA.Enum.STATUS_IDLE

		--Holstering
		if stat == TFA.Enum.STATUS_HOLSTER then
			finalstat = TFA.Enum.STATUS_HOLSTER_READY
			self:SetStatusEnd(ct)
		elseif stat == TFA.Enum.STATUS_HOLSTER_READY then
			self2.FinishHolster(self)
			finalstat = TFA.Enum.STATUS_HOLSTER_FINAL
			self:SetStatusEnd(ct + 0.6)
		elseif stat == TFA.Enum.STATUS_RELOADING_LOOP_START_EMPTY then
			--Shotgun Reloading from empty
			if not self2.IsJammed(self) then
				self2.InsertPrimaryAmmo(self, self2.GetStatL(self, "LoopedReloadInsertAmount", 1))
			end

			if self2.Ammo1(self) <= 0 or self:Clip1() >= self2.GetPrimaryClipSize(self) or self:GetReloadLoopCancel() then
				finalstat = TFA.Enum.STATUS_RELOADING_LOOP_END
				local _, tanim, ttype = self2.ChooseShotgunPumpAnim(self)
				self:SetStatusEnd(ct + self2.GetActivityLength(self, tanim, false, ttype))
				self:SetReloadLoopCancel(false)

				if not self:GetReloadLoopCancel() then
					self:SetJammed(false)
				end
			else
				lact = self:GetLastActivity()
				waittime = self2.GetActivityLength(self, lact, false) - self2.GetActivityLength(self, lact, true)

				if waittime > 0.01 then
					finalstat = TFA.Enum.STATUS_RELOADING_WAIT
					self:SetStatusEnd(ct + waittime)
				else
					finalstat = self2.LoadShell(self)
				end

				self:SetJammed(false)
				--finalstat = self:LoadShell()
				--self:SetStatusEnd( self:GetNextPrimaryFire() )
			end
		elseif stat == TFA.Enum.STATUS_RELOADING_LOOP_START then
			--Shotgun Reloading
			finalstat = self2.LoadShell(self)
		elseif stat == TFA.Enum.STATUS_RELOADING_LOOP then
			self2.InsertPrimaryAmmo(self, self2.GetStatL(self, "LoopedReloadInsertAmount", 1))
			lact = self:GetLastActivity()

			if self2.GetActivityLength(self, lact, true) < self2.GetActivityLength(self, lact, false) - 0.01 then
				local sht = self2.GetStatL(self, "LoopedReloadInsertTime")

				if sht then
					sht = sht / self2.GetAnimationRate(self, ACT_VM_RELOAD)
				end

				waittime = (sht or self2.GetActivityLength(self, lact, false)) - self2.GetActivityLength(self, lact, true)
			else
				waittime = 0
			end

			if waittime > 0.01 then
				finalstat = TFA.Enum.STATUS_RELOADING_WAIT
				self:SetStatusEnd(ct + waittime)
			else
				if self2.Ammo1(self) <= 0 or self:Clip1() >= self:GetPrimaryClipSize() or self:GetReloadLoopCancel() then
					finalstat = TFA.Enum.STATUS_RELOADING_LOOP_END
					local _, tanim, ttype = self2.ChooseShotgunPumpAnim(self)
					self:SetStatusEnd(ct + self2.GetActivityLength(self, tanim, false, ttype))
					self:SetReloadLoopCancel(false)
				else
					finalstat = self2.LoadShell(self)
				end
			end
		elseif stat == TFA.Enum.STATUS_RELOADING then
			self2.CompleteReload(self)
			lact = self:GetLastActivity()
			waittime = self2.GetActivityLength(self, lact, false) - self2.GetActivityLength(self, lact, true)

			if waittime > 0.01 then
				finalstat = TFA.Enum.STATUS_RELOADING_WAIT
				self:SetStatusEnd(ct + waittime)
			end
		elseif stat == TFA.Enum.STATUS_SILENCER_TOGGLE then
			--self:SetStatusEnd( self:GetNextPrimaryFire() )
			self:SetSilenced(not self:GetSilenced())
			self2.Silenced = self:GetSilenced()
		elseif stat == TFA.Enum.STATUS_RELOADING_WAIT and self:GetStatL("LoopedReload") then
			if self2.Ammo1(self) <= 0 or self:Clip1() >= self:GetPrimaryClipSize() or self:GetReloadLoopCancel() then
				finalstat = TFA.Enum.STATUS_RELOADING_LOOP_END
				local _, tanim, ttype = self2.ChooseShotgunPumpAnim(self)
				self:SetStatusEnd(ct + self2.GetActivityLength(self, tanim, false, ttype))
				--self:SetReloadLoopCancel( false )
			else
				finalstat = self2.LoadShell(self)
			end
		elseif stat == TFA.Enum.STATUS_RELOADING_LOOP_END and self:GetStatL("LoopedReload") then
			self:SetReloadLoopCancel(false)
		elseif self2.GetStatL(self, "PumpAction") and stat == TFA.Enum.STATUS_PUMP then
			self:SetReloadLoopCancel(false)
		elseif stat == TFA.Enum.STATUS_SHOOTING and self2.GetStatL(self, "PumpAction") then
			if self:Clip1() == 0 and self2.GetStatL(self, "PumpAction").value_empty then
				--finalstat = TFA.Enum.STATUS_PUMP_READY
				self:SetReloadLoopCancel(true)
			elseif (self2.GetStatL(self, "Primary.ClipSize") < 0 or self:Clip1() > 0) and self2.GetStatL(self, "PumpAction").value then
				--finalstat = TFA.Enum.STATUS_PUMP_READY
				self:SetReloadLoopCancel(true)
			end
		end

		--self:SetStatusEnd( math.huge )
		self:SetStatus(finalstat)

		local sightsMode = self2.GetStatL(self, "Sights_Mode")
		local sprintMode = self2.GetStatL(self, "Sprint_Mode")
		local walkMode = self2.GetStatL(self, "Walk_Mode")
		local customizeMode = self2.GetStatL(self, "Customize_Mode")

		local smi = sightsMode ~= TFA.Enum.LOCOMOTION_LUA
		local spi = sprintMode ~= TFA.Enum.LOCOMOTION_LUA
		local wmi = walkMode ~= TFA.Enum.LOCOMOTION_LUA
		local cmi = customizeMode ~= TFA.Enum.LOCOMOTION_LUA

		if
			not TFA.Enum.ReadyStatus[stat] and
			stat ~= TFA.Enum.STATUS_SHOOTING and
			stat ~= TFA.Enum.STATUS_PUMP and
			finalstat == TFA.Enum.STATUS_IDLE and
			((smi or spi) or (cst and cmi))
		then
			is = self2.GetIronSights(self, true)

			if (is and smi) or (spr and spi) or (wlk and wmi) or (cst and cmi) then
				local success, _ = self2.Locomote(self, is and smi, is, spr and spi, spr, wlk and wmi, wlk, cst and cmi, cst)

				if success == false then
					self:SetNextIdleAnim(-1)
				else
					self:SetNextIdleAnim(math.max(self:GetNextIdleAnim(), ct + 0.1))
				end
			end
		end

		self2.LastBoltShoot = nil

		if self:GetBurstCount() > 0 then
			if finalstat ~= TFA.Enum.STATUS_SHOOTING and finalstat ~= TFA.Enum.STATUS_IDLE then
				self:SetBurstCount(0)
			elseif self:GetBurstCount() < self:GetMaxBurst() and self:Clip1() > 0 then
				self:PrimaryAttack()
			else
				self:SetBurstCount(0)
				self:SetNextPrimaryFire(self2.GetNextCorrectedPrimaryFire(self, self2.GetBurstDelay(self)))
			end
		end
	end

	--if stat == TFA.Enum.STATUS_IDLE and self:GetReloadLoopCancel() and (self2.GetStatL(self, "AllowSprintAttack") or self:GetSprintProgress() < 0.1) then
	if stat == TFA.Enum.STATUS_IDLE and self:GetReloadLoopCancel() then
		if self2.GetStatL(self, "PumpAction") then
			if ct > self:GetNextPrimaryFire() and not self:KeyDown(IN_ATTACK) then
				self2.DoPump(self)
			end
		else
			self:SetReloadLoopCancel(false)
		end
	end
end
