-- Degrees to accuracy vector, Valve's formula from SDK 2013
TFA.DegreesToAccuracy = math.sin((math.pi / 180) / 2) -- approx. 0.00873

--default cvar integration
local cv_gravity = GetConVar("sv_gravity")

--[[local function TimeScale(v)
	return v * game.GetTimeScale() / TFA.Ballistics.SubSteps
end]]

--init code
TFA.Ballistics = TFA.Ballistics or {}
TFA.Ballistics.Enabled = false
TFA.Ballistics.Gravity = Vector(0, 0, -cv_gravity:GetFloat())
TFA.Ballistics.Bullets = TFA.Ballistics.Bullets or {}
TFA.Ballistics.Bullets.bullet_registry = TFA.Ballistics.Bullets.bullet_registry or {}
TFA.Ballistics.BulletLife = 10
TFA.Ballistics.UnitScale = TFA.UnitScale or 39.3701 --meters to inches
TFA.Ballistics.AirResistance = 1
TFA.Ballistics.WaterResistance = 3
TFA.Ballistics.WaterEntranceResistance = 6

TFA.Ballistics.DamageVelocityLUT = {
	[13] = 350, --shotgun
	[25] = 425, --mp5k etc.
	[35] = 900, --ak-12
	[65] = 830, --SVD
	[120] = 1100 --sniper cap
}

TFA.Ballistics.VelocityMultiplier = 1
TFA.Ballistics.SubSteps = 1
TFA.Ballistics.BulletCreationNetString = "TFABallisticsBullet"

TFA.Ballistics.TracerStyles = {
	[0] = "",
	[1] = "tfa_bullet_smoke_tracer",
	[2] = "tfa_bullet_fire_tracer"
}

setmetatable(TFA.Ballistics.TracerStyles, {
	["__index"] = function(t, k) return t[math.Round(tonumber(k) or 1)] or t[1] end
})

if SERVER then
	util.AddNetworkString(TFA.Ballistics.BulletCreationNetString)
end

--bullet class
local function IncludeClass(fn)
	include("tfa/ballistics/" .. fn .. ".lua")
	AddCSLuaFile("tfa/ballistics/" .. fn .. ".lua")
end

IncludeClass("bullet")
--cvar code
local function CreateReplConVar(cvarname, cvarvalue, description, ...)
	return CreateConVar(cvarname, cvarvalue, CLIENT and {FCVAR_REPLICATED} or {FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, description, ...)
end -- replicated only on clients, archive/notify on server

local cv_enabled = CreateReplConVar("sv_tfa_ballistics_enabled", "0", "Enable TFA Ballistics?")
local cv_bulletlife = CreateReplConVar("sv_tfa_ballistics_bullet_life", 10, "Time to process bullets before removing.")
local cv_res_air = CreateReplConVar("sv_tfa_ballistics_bullet_damping_air", 1, "Air resistance, which makes bullets arc faster.")
local cv_res_water = CreateReplConVar("sv_tfa_ballistics_bullet_damping_water", 3, "Water resistance, which makes bullets arc faster in water.")
local cv_vel = CreateReplConVar("sv_tfa_ballistics_bullet_velocity", 1, "Global velocity multiplier for TFA ballistics bullets.")
local cv_substep = CreateReplConVar("sv_tfa_ballistics_substeps", 1, "Substeps for ballistics; more is more precise, at the cost of performance.")
local sv_tfa_ballistics_custom_gravity = CreateReplConVar("sv_tfa_ballistics_custom_gravity", 0, "Enable sv_gravity override for ballistics")
local sv_tfa_ballistics_custom_gravity_value = CreateReplConVar("sv_tfa_ballistics_custom_gravity_value", 0, "Z velocity down of custom gravity")
CreateReplConVar("sv_tfa_ballistics_mindist", -1, "Minimum distance to activate; -1 for always.")

local function updateCVars()
	TFA.Ballistics.BulletLife = cv_bulletlife:GetFloat()
	TFA.Ballistics.AirResistance = cv_res_air:GetFloat()
	TFA.Ballistics.WaterResistance = cv_res_water:GetFloat()
	TFA.Ballistics.WaterEntranceResistance = TFA.Ballistics.WaterResistance * 2
	TFA.Ballistics.VelocityMultiplier = cv_vel:GetFloat()

	if sv_tfa_ballistics_custom_gravity:GetBool() then
		TFA.Ballistics.Gravity.z = -sv_tfa_ballistics_custom_gravity_value:GetFloat()
	else
		TFA.Ballistics.Gravity.z = -cv_gravity:GetFloat()
	end

	TFA.Ballistics.Enabled = cv_enabled:GetBool()
	TFA.Ballistics.SubSteps = cv_substep:GetInt()
end

cvars.AddChangeCallback("sv_tfa_ballistics_enabled", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_bullet_life", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_bullet_damping_air", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_bullet_damping_water", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_bullet_velocity", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_substeps", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_mindist", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_custom_gravity", updateCVars, "TFA")
cvars.AddChangeCallback("sv_tfa_ballistics_custom_gravity_value", updateCVars, "TFA")
cvars.AddChangeCallback("sv_gravity", updateCVars, "TFA Ballistics")
updateCVars()

--client cvar code
local cv_receive, cv_tracers_style, cv_tracers_mp

if CLIENT then
	cv_receive = CreateClientConVar("cl_tfa_ballistics_mp", "1", true, false, "Receive bullet data from other players?")
	CreateClientConVar("cl_tfa_ballistics_fx_bullet", "1", true, false, "Display bullet models for each TFA ballistics bullet?")
	cv_tracers_style = CreateClientConVar("cl_tfa_ballistics_fx_tracers_style", "1", true, false, "Style of tracers for TFA ballistics? 0=disable,1=smoke")
	cv_tracers_mp = CreateClientConVar("cl_tfa_ballistics_fx_tracers_mp", "1", true, false, "Enable tracers for other TFA ballistics users?")
	CreateClientConVar("cl_tfa_ballistics_fx_tracers_adv", "1", true, false, "Enable advanced tracer calculations for other users? This corrects smoke trail to their barrel")
end

--utility func
local function Remap(inp, u, v, x, y)
	return (inp - u) / (v - u) * (y - x) + x
end

--Accessors
local CopyTable = table.Copy

function TFA.Ballistics.Bullets:Add(bulletStruct, originalBulletData)
	local bullet = TFA.Ballistics:Bullet(bulletStruct)
	bullet.bul = CopyTable(originalBulletData or bullet.bul)
	bullet.last_update = CurTime() - TFA.FrameTime()

	table.insert(self.bullet_registry, bullet)

	bullet:_setup()

	if SERVER and game.GetTimeScale() > 0.99 then
		-- always update bullet since they are being added from predicted hook
		bullet:Update(CurTime())
	end
end

function TFA.Ballistics.Bullets:Update(ply)
	--local delta = TimeScale(SysTime() - (self.lastUpdate or (SysTime() - FrameTime())))
	local delta = CurTime()

	--self.lastUpdate = SysTime()
	local toremove
	local lply = CLIENT and LocalPlayer()

	for i, bullet in ipairs(self.bullet_registry) do
		if bullet.delete then
			if not toremove then
				toremove = {}
			end

			table.insert(toremove, i)
		elseif not ply and not bullet.playerOwned or CLIENT and bullet.owner ~= lply or ply == bullet.owner then
			for _ = 1, TFA.Ballistics.SubSteps do
				bullet:Update(delta)
			end
		end
	end

	if toremove then
		for i = #toremove, 1, -1 do
			table.remove(self.bullet_registry, toremove[i])
		end
	end
end

function TFA.Ballistics:AutoDetectVelocity(damage)
	local lutMin, lutMax, LUT, DMGs
	LUT = self.DamageVelocityLUT
	DMGs = table.GetKeys(LUT)
	table.sort(DMGs)

	for _, v in ipairs(DMGs) do
		if v < damage then
			lutMin = v
		elseif lutMin then
			lutMax = v
			break
		end
	end

	if not lutMax then
		lutMax = DMGs[#DMGs]
		lutMin = DMGs[#DMGs - 1]
	elseif not lutMin then
		lutMin = DMGs[1]
		lutMax = DMGs[2]
	end

	return Remap(damage, lutMin, lutMax, LUT[lutMin], LUT[lutMax])
end

function TFA.Ballistics:ShouldUse(wep)
	if not IsValid(wep) or not wep.IsTFAWeapon then
		return false
	end

	local shouldUse = wep:GetStatL("UseBallistics")

	if shouldUse == nil then
		if wep:GetStatL("TracerPCF") then
			return false
		end

		return self.Enabled
	else
		return shouldUse
	end
end

local sv_tfa_recoil_legacy = GetConVar("sv_tfa_recoil_legacy")

function TFA.Ballistics:FireBullets(wep, bulletStruct, angIn, bulletOverride)
	if not IsValid(wep) then return end
	if not IsValid(wep:GetOwner()) then return end

	local vel

	if bulletStruct.Velocity then
		vel = bulletStruct.Velocity
	elseif wep.GetStat and wep:GetStatL("Primary.Velocity") then
		vel = wep:GetStatL("Primary.Velocity") * TFA.Ballistics.UnitScale
	elseif wep.Primary and wep.Primary.Velocity then
		vel = wep.Primary.Velocity * TFA.Ballistics.UnitScale
	elseif wep.Velocity then
		vel = wep.Velocity * TFA.Ballistics.UnitScale
	else
		local dmg

		if wep.GetStat and wep:GetStatL("Primary.Damage") then
			dmg = wep:GetStatL("Primary.Damage")
		else
			dmg = wep.Primary.Damage or wep.Damage or 30
		end

		vel = TFA.Ballistics:AutoDetectVelocity(dmg) * TFA.Ballistics.UnitScale
	end

	vel = vel * (TFA.Ballistics.VelocityMultiplier or 1)

	local oldNum = bulletStruct.Num
	bulletStruct.Num = 1
	bulletStruct.IsBallistics = true

	local owner = wep:GetOwner()
	local isnpc = owner:IsNPC()
	local ac = bulletStruct.Spread
	local sharedRandomSeed = "Ballistics" .. CurTime()

	for i = 1, oldNum do
		local ang

		if angIn then
			ang = angIn
		else
			ang = owner:GetAimVector():Angle()

			if sv_tfa_recoil_legacy:GetBool() and not isnpc then
				ang:Add(owner:GetViewPunchAngles())
			else
				ang.p = ang.p + wep:GetViewPunchP()
				ang.y = ang.y + wep:GetViewPunchY()
			end
		end

		if not angIn then
			ang:RotateAroundAxis(ang:Up(), util.SharedRandom(sharedRandomSeed, -ac.x * 45, ac.x * 45, 0 + i))
			ang:RotateAroundAxis(ang:Right(), util.SharedRandom(sharedRandomSeed, -ac.y * 45, ac.y * 45, 1 + i))
		end

		local struct = {
			owner = owner, --used for dmginfo SetAttacker
			inflictor = wep, --used for dmginfo SetInflictor
			damage = bulletStruct.Damage, --floating point number representing inflicted damage
			force = bulletStruct.Force,
			pos = bulletOverride and bulletStruct.Src or owner:GetShootPos(), --b.Src, --vector representing current position
			velocity = (bulletOverride and bulletStruct.Dir or ang:Forward()) * vel, --b.Dir * vel, --vector representing movement velocity
			model = wep.BulletModel or bulletStruct.Model, --optional variable representing the given model
			smokeparticle = bulletStruct.SmokeParticle,
			customPosition = bulletStruct.CustomPosition or bulletOverride,
			IgnoreEntity = bulletStruct.IgnoreEntity
		}

		if CLIENT then
			if not struct.smokeparticle then
				struct.smokeparticle = TFA.Ballistics.TracerStyles[cv_tracers_style:GetInt()]
			end
		end

		self.Bullets:Add(struct, bulletStruct)

		if SERVER then
			net.Start(TFA.Ballistics.BulletCreationNetString)

			net.WriteEntity(struct.owner)
			net.WriteEntity(struct.inflictor)
			net.WriteFloat(struct.damage)
			net.WriteFloat(struct.force)
			net.WriteVector(struct.pos)

			net.WriteDouble(struct.velocity.x)
			net.WriteDouble(struct.velocity.y)
			net.WriteDouble(struct.velocity.z)

			net.WriteString(struct.model or '')
			net.WriteString(struct.smokeparticle or '')
			net.WriteBool(struct.customPosition == true)
			net.WriteEntity(struct.IgnoreEntity or NULL)

			net.WriteVector(bulletStruct.Src)
			net.WriteNormal(bulletStruct.Dir)
			net.WriteEntity(bulletStruct.Attacker)
			net.WriteVector(bulletStruct.Spread)
			net.WriteFloat(vel)

			if game.SinglePlayer() or isnpc then
				net.SendPVS(struct.pos)
			else
				net.SendOmit(owner)
			end
		end
	end
end

function TFA.Ballistics.Bullets:Render()
	for i = 1, #self.bullet_registry do
		self.bullet_registry[i]:Render()
	end
end

local sp = game.SinglePlayer()

--Netcode and Hooks
if CLIENT then
	net.Receive(TFA.Ballistics.BulletCreationNetString, function()
		if not sp and not cv_receive:GetBool() then return end

		local owner =           net.ReadEntity()
		local inflictor =       net.ReadEntity()
		local damage =          net.ReadFloat()
		local force =           net.ReadFloat()
		local pos =             net.ReadVector()
		local velocity =        Vector(net.ReadDouble(), net.ReadDouble(), net.ReadDouble())
		local model =           net.ReadString()
		local smokeparticle =   net.ReadString()
		local customPosition =  net.ReadBool()
		local IgnoreEntity =    net.ReadEntity()


		local Src =             net.ReadVector()
		local Dir =             net.ReadNormal()
		local Attacker =        net.ReadEntity()
		local Spread =          net.ReadVector()
		local Velocity =        net.ReadFloat()

		if not IsValid(owner) or not IsValid(inflictor) then return end

		if not cv_tracers_mp:GetBool() and owner ~= LocalPlayer() then
			smokeparticle = ""
		elseif smokeparticle == "" then
			smokeparticle = TFA.Ballistics.TracerStyles[cv_tracers_style:GetInt()]
		end

		local struct = {
			owner = owner,
			inflictor = inflictor,
			damage = damage,
			force = force,
			pos = pos,
			velocity = velocity,
			model = model ~= "" and model or nil,
			smokeparticle = smokeparticle,
			customPosition = customPosition,
			IgnoreEntity = IgnoreEntity,
		}

		local bulletStruct = {
			Damage = damage,
			Force = force,
			Num = 1,
			Src = Src,
			Dir = Dir,
			Attacker = Attacker,
			Spread = Spread,
			SmokeParticle = smokeparticle,
			CustomPosition = customPosition,
			Model = model ~= "" and model or nil,
			Velocity = Velocity,
			IsBallistics = true,
		}

		TFA.Ballistics.Bullets:Add(struct, bulletStruct)
	end)
end

if CLIENT then
	hook.Add("FinishMove", "TFABallisticsTick", function(self)
		if IsFirstTimePredicted() then
			TFA.Ballistics.Bullets:Update(self)
		end
	end)
else
	hook.Add("PlayerPostThink", "TFABallisticsTick", function(self)
		TFA.Ballistics.Bullets:Update(self)
	end)
end

hook.Add("Tick", "TFABallisticsTick", function()
	TFA.Ballistics.Bullets:Update()

	if CLIENT and sp then
		TFA.Ballistics.Bullets:Update(LocalPlayer())
	end
end)

--Rendering
hook.Add("PostDrawOpaqueRenderables", "TFABallisticsRender", function()
	TFA.Ballistics.Bullets:Render()
end)
