AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")

include("shared.lua")

--===============================================================================================--
-- Local Funcs and Vars
--===============================================================================================--

local Damage      = ACF.Damage
local Contraption = ACF.Contraption
local Objects     = Damage.Objects
local ActiveTanks = ACF.FuelTanks
local Utilities   = ACF.Utilities
local Clock       = Utilities.Clock
local Sounds      = Utilities.Sounds
local RefillDist  = ACF.RefillDistance * ACF.RefillDistance
local TimerCreate = timer.Create
local TimerExists = timer.Exists
local Clamp       = math.Clamp
local Round       = math.Round
local HookRun     = hook.Run

local function CanRefuel(Refill, Tank, Distance)
	if Refill == Tank then return false end
	if Refill.FuelType ~= Tank.FuelType then return false end
	if Tank.Disabled then return false end
	if Tank.SupplyFuel then return false end
	if Tank.Fuel >= Tank.Capacity then return false end

	return Distance <= RefillDist
end

--===============================================================================================--

do -- Spawn and Update functions
	local Classes   = ACF.Classes
	local WireIO    = Utilities.WireIO
	local Entities  = Classes.Entities
	local FuelTanks = Classes.FuelTanks
	local FuelTypes = Classes.FuelTypes

	local Inputs = {
		"Active (If set to a non-zero value, it'll allow engines to consume fuel from this fuel tank.)",
		"Refuel Duty (If set to a non-zero value, this fuel tank will refill surrounding tanks that contain the same fuel type.)",
	}
	local Outputs = {
		"Activated (Whether or not this fuel tank is able to be used by an engine.)",
		"Fuel (Amount of fuel currently in the tank, in liters or kWh)",
		"Capacity (Total amount of fuel the tank can hold, in liters or kWh)",
		"Leaking (Returns 1 if the fuel tank is currently losing fuel.)",
		"Entity (The fuel tank itself.) [ENTITY]"
	}

	local function VerifyData(Data)
		if not isstring(Data.FuelTank) then
			Data.FuelTank = Data.SizeId or Data.Id
		end

		local Class = Classes.GetGroup(FuelTanks, Data.FuelTank)

		if not Class then
			Data.FuelTank = "Box"

			Class = FuelTanks.Get("FTS_B")
		elseif FuelTanks.IsAlias(Data.FuelTank) then
			Data.FuelTank = Class.ID
		end

		if Class.IsScalable then
			local FuelTank = FuelTanks.GetItem(Class.ID, Data.FuelTank)

			if FuelTank and FuelTank.Size then
				Data.FuelTank = Class.ID
				Data.Size     = Vector(FuelTank.Size)
			elseif not isvector(Data.Size) then
				local X = ACF.CheckNumber(Data.TankSizeX, 24)
				local Y = ACF.CheckNumber(Data.TankSizeY, 24)
				local Z = ACF.CheckNumber(Data.TankSizeZ, 24)

				Data.Size = Vector(X, Y, Z)
			end

			do -- Clamping size
				local Min  = ACF.FuelMinSize
				local Max  = ACF.FuelMaxSize
				local Size = Data.Size

				Size.x = Clamp(Round(Size.x), Min, Max)
				Size.y = Clamp(Round(Size.y), Min, Max)
				Size.z = Clamp(Round(Size.z), Min, Max)
			end
		else
			Data.Size = nil
		end

		-- Making sure to provide a valid fuel type
		if not FuelTypes.Get(Data.FuelType) then
			Data.FuelType = "Petrol"
		end

		do -- External verifications
			if Class.VerifyData then
				Class.VerifyData(Data, Class)
			end

			HookRun("ACF_OnVerifyData", "acf_fueltank", Data, Class)
		end
	end

	local function UpdateFuelTank(Entity, Data, Class, FuelTank, FuelType)
		-- If updating, keep the same fuel level
		local Percentage = Entity.Capacity and Entity.Fuel / Entity.Capacity or 1
		local Material = Class.Material or FuelTank and FuelTank.Model

		Entity.ACF = Entity.ACF or {}
		Entity.ACF.Model = FuelTank and FuelTank.Model or Class.Model -- Must be set before changing model
		Entity.ClassData = Class

		Entity:SetScaledModel(Entity.ACF.Model)
		Entity:SetSize(Class.IsScalable and Data.Size or Entity:GetOriginalSize())
		Entity:SetMaterial(Material or "")

		-- Storing all the relevant information on the entity for duping
		for _, V in ipairs(Entity.DataStore) do
			Entity[V] = Data[V]
		end

		local Volume, Area
		local Wall = ACF.FuelArmor * ACF.MmToInch -- Wall thickness in inches

		if Class.CalcVolume then
			Volume, Area = Class.CalcVolume(Data.Size, Wall)
		else -- Default to finding surface area/volume based off physics object instead
			local PhysObj = Entity:GetPhysicsObject()

			Area = PhysObj:GetSurfaceArea()
			Volume = PhysObj:GetVolume() - (Area * Wall) -- Total volume of tank (cu in), reduced by wall thickness
		end

		local NameType = " " .. (Class.NameType or FuelTank.Name)

		Entity.Name        = Entity.FuelType .. NameType
		Entity.ShortName   = Entity.FuelType
		Entity.EntType     = Class.Name
		Entity.FuelDensity = FuelType.Density
		Entity.Capacity    = Volume * ACF.gCmToKgIn * ACF.TankVolumeMul -- Internal volume available for fuel in liters
		Entity.EmptyMass   = (Area * Wall) * ACF.InchToCmCu * ACF.SteelDensity -- Total wall volume * cu in to cc * density of steel (kg/cc)
		Entity.IsExplosive = FuelTank and FuelTank.IsExplosive or Class.IsExplosive
		Entity.NoLinks     = FuelTank and FuelTank.Unlinkable or Class.Unlinkable
		Entity.Shape       = FuelTank and FuelTank.Shape or Class.Shape

		WireIO.SetupInputs(Entity, Inputs, Data, Class, FuelTank, FuelType)
		WireIO.SetupOutputs(Entity, Outputs, Data, Class, FuelTank, FuelType)

		if Entity.FuelType == "Electric" then
			Entity.Name = "Electric Battery"
			Entity.Liters = Entity.Capacity -- Batteries capacity is different from internal volume
			Entity.Capacity = Entity.Capacity * ACF.LiIonED
		end

		Entity:SetNWString("WireName", "ACF " .. Entity.Name)

		Entity.Fuel = Percentage * Entity.Capacity

		ACF.Activate(Entity, true)

		Entity:UpdateMass(true)

		WireLib.TriggerOutput(Entity, "Fuel", Entity.Fuel)
		WireLib.TriggerOutput(Entity, "Capacity", Entity.Capacity)
	end

	function ACF.MakeFuelTank(Player, Pos, Angle, Data)
		VerifyData(Data)

		local Class    = Classes.GetGroup(FuelTanks, Data.FuelTank)
		local FuelTank = FuelTanks.GetItem(Class.ID, Data.FuelTank)
		local FuelType = FuelTypes.Get(Data.FuelType)
		local Limit    = Class.LimitConVar.Name
		local Model    = Class.Model or FuelTank and FuelTank.Model
		local Material = Class.Material or FuelTank and FuelTank.Model

		if not Player:CheckLimit(Limit) then return end

		local CanSpawn = HookRun("ACF_PreSpawnEntity", "acf_fueltank", Player, Data, Class, FuelTank)

		if CanSpawn == false then return end

		local Tank = ents.Create("acf_fueltank")

		if not IsValid(Tank) then return end

		Tank.ACF		= Tank.ACF or {}

		Tank:SetScaledModel(Model)
		if Material then
			Tank:SetMaterial(Material)
		end
		Tank:SetAngles(Angle)
		Tank:SetPos(Pos)
		Tank:Spawn()

		Player:AddCleanup("acf_fueltank", Tank)
		Player:AddCount(Limit, Tank)

		Tank.Engines       = {}
		Tank.Leaking       = 0
		Tank.LastThink     = 0
		Tank.LastFuel      = 0
		Tank.LastActivated = 0
		Tank.DataStore     = Entities.GetArguments("acf_fueltank")

		duplicator.ClearEntityModifier(Tank, "mass")

		UpdateFuelTank(Tank, Data, Class, FuelTank, FuelType)

		if Class.OnSpawn then
			Class.OnSpawn(Tank, Data, Class, FuelTank)
		end

		HookRun("ACF_OnSpawnEntity", "acf_fueltank", Tank, Data, Class, FuelTank)

		-- Fuel tanks should be active by default
		Tank:TriggerInput("Active", 1)

		ActiveTanks[Tank] = true

		ACF.CheckLegal(Tank)

		return Tank
	end

	Entities.Register("acf_fueltank", ACF.MakeFuelTank, "FuelTank", "FuelType", "Size")

	ACF.RegisterLinkSource("acf_fueltank", "Engines")

	------------------- Updating ---------------------

	function ENT:Update(Data)
		VerifyData(Data)

		local Class    = Classes.GetGroup(FuelTanks, Data.FuelTank)
		local FuelTank = FuelTanks.GetItem(Class.ID, Data.FuelTank)
		local FuelType = FuelTypes.Get(Data.FuelType)
		local OldClass = self.ClassData
		local Feedback = ""

		local CanUpdate, Reason = HookRun("ACF_PreUpdateEntity", "acf_fueltank", self, Data, Class, FuelTank)

		if CanUpdate == false then return CanUpdate, Reason end

		if OldClass.OnLast then
			OldClass.OnLast(self, OldClass)
		end

		HookRun("ACF_OnEntityLast", "acf_fueltank", self, OldClass)

		ACF.SaveEntity(self)

		UpdateFuelTank(self, Data, Class, FuelTank, FuelType)

		ACF.RestoreEntity(self)

		if Class.OnUpdate then
			Class.OnUpdate(self, Data, Class, FuelTank)
		end

		HookRun("ACF_OnUpdateEntity", "acf_fueltank", self, Data, Class, FuelTank)

		if next(self.Engines) then
			local Fuel    = self.FuelType
			local NoLinks = self.NoLinks
			local Count, Total = 0, 0

			for Engine in pairs(self.Engines) do
				if NoLinks or not Engine.FuelTypes[Fuel] then
					self:Unlink(Engine)

					Count = Count + 1
				end

				Total = Total + 1
			end

			if Count == Total then
				Feedback = "\nUnlinked from all engines due to fuel type or model change."
			elseif Count > 0 then
				local Text = "\nUnlinked from %s out of %s engines due to fuel type or model change."

				Feedback = Text:format(Count, Total)
			end
		end

		return true, "Fuel tank updated successfully!" .. Feedback
	end

	hook.Add("cfw.contraption.entityAdded", "ACF_CFWFuelIndex", function(contraption, ent)
		if ent:GetClass() == "acf_fuel" then
			contraption.Fuels = contraption.Fuels or {}
			contraption.Fuels[ent] = true
		end
	end)

	hook.Add("cfw.contraption.entityRemoved", "ACF_CFWFuelUnIndex", function(contraption, ent)
		if ent:GetClass() == "acf_fuel" then
			contraption.Fuels = contraption.Fuels or {}
			contraption.Fuels[ent] = nil
		end
	end)
end

--===============================================================================================--
-- Meta Funcs
--===============================================================================================--

function ENT:ACF_Activate(Recalc)
	local PhysObj = self:GetPhysicsObject()
	local Area    = PhysObj:GetSurfaceArea() * ACF.InchToCmSq
	local Armour  = self.EmptyMass * 1000 / Area / 0.78 * ACF.ArmorMod -- So we get the equivalent thickness of that prop in mm if all it's weight was a steel plate
	local Health  = Area / ACF.Threshold
	local Percent = 1

	if Recalc and self.ACF.Health and self.ACF.MaxHealth then
		Percent = self.ACF.Health / self.ACF.MaxHealth
	end

	self.ACF.Area      = Area
	self.ACF.Health    = Health * Percent
	self.ACF.MaxHealth = Health
	self.ACF.Armour    = Armour * (0.5 + Percent * 0.5)
	self.ACF.MaxArmour = Armour
	self.ACF.Type      = "Prop"
end

function ENT:ACF_OnDamage(DmgResult, DmgInfo)
	local HitRes    = Damage.doPropDamage(self, DmgResult, DmgInfo) -- Calling the standard prop damage function
	local Inflictor = DmgInfo:GetInflictor()
	local NoExplode = self.FuelType == "Diesel"

	if self.Exploding or NoExplode or not self.IsExplosive then return HitRes end

	local Attacker  = DmgInfo:GetAttacker()

	if HitRes.Kill then
		local CanExplode = HookRun("ACF_PreExplodeFuel", self)

		if not CanExplode then return HitRes end

		if IsValid(Attacker) and Attacker:IsPlayer() then
			self.Attacker = Attacker
		end

		if IsValid(Inflictor) and Inflictor:IsPlayer() then
			self.Inflictor = Inflictor
		end

		self:Detonate()

		return HitRes
	end

	local Ratio = (HitRes.Damage / self.ACF.Health) ^ 0.75 -- Chance to explode from sheer damage, small shots = small chance
	local ExplodeChance = (1 - (self.Fuel / self.Capacity)) ^ 0.75 -- Chance to explode from fumes in tank, less fuel = more explodey

	-- It's gonna blow
	if math.random() < (ExplodeChance + Ratio) then
		local CanExplode = HookRun("ACF_PreExplodeFuel", self)

		if not CanExplode then return HitRes end

		self.Attacker = Attacker
		self.Inflictor = Inflictor

		self:Detonate()
	else -- Spray some fuel around
		self.Leaking = self.Leaking + self.Fuel * ((HitRes.Damage / self.ACF.Health) ^ 1.5) * 0.25

		WireLib.TriggerOutput(self, "Leaking", self.Leaking > 0 and 1 or 0)

		self:NextThink(Clock.CurTime + 0.1)
	end

	return HitRes
end

function ENT:Detonate()
	if self.Exploding then return end

	self.Exploding = true -- Prevent multiple explosions

	local Position  = self:LocalToWorld(self:OBBCenter() + VectorRand() * (self:OBBMaxs() - self:OBBMins()) / 2)
	local Explosive = (math.max(self.Fuel, self.Capacity * 0.0025) / self.FuelDensity) * 0.1
	local DmgInfo   = Objects.DamageInfo(self.Attacker or self, self.Inflictor)

	ACF.KillChildProps(self, Position, Explosive)

	Damage.createExplosion(Position, Explosive, Explosive * 0.5, { self }, DmgInfo)
	Damage.explosionEffect(Position, nil, Explosive)

	constraint.RemoveAll(self)
	self:Remove()
end

function ENT:Enable()
	WireLib.TriggerOutput(self, "Activated", self:CanConsume() and 1 or 0)
end

function ENT:Disable()
	WireLib.TriggerOutput(self, "Activated", 0)
end

do -- Mass Update
	local function UpdateMass(Entity, SelfTbl)
		SelfTbl = SelfTbl or Entity:GetTable()
		local Fuel    = SelfTbl.FuelType == "Electric" and SelfTbl.Liters or SelfTbl.Fuel
		local Mass    = math.floor(SelfTbl.EmptyMass + Fuel * SelfTbl.FuelDensity)

		Contraption.SetMass(Entity, Mass)
	end

	function ENT:UpdateMass(Instant, SelfTbl)
		SelfTbl = SelfTbl or self:GetTable()
		if Instant then
			return UpdateMass(self, SelfTbl)
		end

		if TimerExists("ACF Mass Buffer" .. self:EntIndex()) then return end

		TimerCreate("ACF Mass Buffer" .. self:EntIndex(), 1, 1, function()
			if not IsValid(self) then return end

			UpdateMass(self, SelfTbl)
		end)
	end
end

do -- Overlay Update
	local Classes = ACF.Classes
	local FuelTypes = Classes.FuelTypes

	local Text = "%s\n\n%sFuel Type: %s\n%s"

	function ENT:UpdateOverlayText()
		local Size = ""
		local Status, Content

		if self.Leaking > 0 then
			Status = "Leaking"
		else
			Status = self:CanConsume() and "Providing Fuel" or "Idle"
		end

		local Class = self.ClassData

		if Class and Class.CalcOverlaySize then
			Size = Class.CalcOverlaySize(self)
		end

		local FuelTypeID = self.FuelType
		local FuelType = FuelTypes.Get(FuelTypeID)
		local Fuel = self.Fuel

		if FuelType and FuelType.FuelTankOverlayText then
			Content = FuelType.FuelTankOverlayText(Fuel)
		else
			local Liters = Round(Fuel, 2)
			local Gallons = Round(Fuel * ACF.LToGal, 2)

			Content = "Fuel Remaining: " .. Liters .. " liters / " .. Gallons .. " gallons"
		end

		return Text:format(Status, Size, FuelTypeID, Content)
	end
end

ACF.AddInputAction("acf_fueltank", "Active", function(Entity, Value)
	Entity.Active = tobool(Value)

	WireLib.TriggerOutput(Entity, "Activated", Entity:CanConsume() and 1 or 0)
end)

ACF.AddInputAction("acf_fueltank", "Refuel Duty", function(Entity, Value)
	Entity.SupplyFuel = tobool(Value) or nil
end)

function ENT:CanConsume(SelfTbl)
	SelfTbl = SelfTbl or self:GetTable()
	if SelfTbl.Disabled then return false end
	if not SelfTbl.Active then return false end

	return SelfTbl.Fuel > 0
end

function ENT:Consume(Amount, SelfTbl)
	SelfTbl = SelfTbl or self:GetTable()
	local Fuel = Clamp(SelfTbl.Fuel - Amount, 0, SelfTbl.Capacity)
	SelfTbl.Fuel = Fuel

	Fuel = Round(Fuel, 2)
	local Activated = self:CanConsume(SelfTbl) and 1 or 0

	if SelfTbl.LastFuel ~= Fuel then
		SelfTbl.LastFuel = Fuel
		WireLib.TriggerOutput(self, "Fuel", Fuel)

		self:UpdateOverlay()
		self:UpdateMass(_, SelfTbl)
	end
	if SelfTbl.LastActivated ~= Activated then
		SelfTbl.LastActivated = Activated
		WireLib.TriggerOutput(self, "Activated", Activated)

		self:UpdateOverlay()
	end
end

do
	local function RefillEffect(Entity)
		net.Start("ACF_RefillEffect")
			net.WriteEntity(Entity)
		net.Broadcast()
	end

	local function StopRefillEffect(Entity)
		net.Start("ACF_StopRefillEffect")
			net.WriteEntity(Entity)
		net.Broadcast()
	end

	function ENT:Think()
		self:NextThink(Clock.CurTime + 1)

		local Leaking = self.Leaking

		if Leaking > 0 then
			self:Consume(Leaking)

			local Fuel = self.Fuel
			Leaking = Clamp(Leaking - (1 / math.max(Fuel, 1)) ^ 0.5, 0, Fuel) -- Fuel tanks are self healing
			self.Leaking = Leaking

			WireLib.TriggerOutput(self, "Leaking", Leaking > 0 and 1 or 0)

			self:NextThink(Clock.CurTime + 0.25)
		end

		if self.Refilling then
			StopRefillEffect(self)
			self.Refilling = false
		end

		-- Refuelling
		if self.SupplyFuel and self:CanConsume() then
			local DeltaTime = Clock.CurTime - self.LastThink
			local Position  = self:GetPos()

			for Tank in pairs(ACF.FuelTanks) do
				local Distance = Position:DistToSqr(Tank:GetPos())

				if CanRefuel(self, Tank, Distance) then
					local Exchange  = math.min(DeltaTime * ACF.RefuelSpeed * ACF.FuelRate, self.Fuel, Tank.Capacity - Tank.Fuel)
					local CanRefill = hook.Run("ACF_PreRefillFuel", self, Tank, Exchange)

					if not CanRefill then continue end

					self:Consume(Exchange)
					Tank:Consume(-Exchange)

					if self.FuelType == "Electric" then
						Sounds.SendSound(self, "ambient/energy/newspark04.wav", 70, 100, 0.5)
						Sounds.SendSound(Tank, "ambient/energy/newspark04.wav", 70, 100, 0.5)
					else
						Sounds.SendSound(self, "vehicles/jetski/jetski_no_gas_start.wav", 70, 120, 0.5)
						Sounds.SendSound(Tank, "vehicles/jetski/jetski_no_gas_start.wav", 70, 120, 0.5)
					end

					RefillEffect(self)
					self.Refilling = true
				end
			end
		end

		self.LastThink = Clock.CurTime

		return true
	end
end

function ENT:OnRemove()
	local Class = self.ClassData

	if Class.OnLast then
		Class.OnLast(self, Class)
	end

	HookRun("ACF_OnEntityLast", "acf_fueltank", self, Class)

	for Engine in pairs(self.Engines) do
		self:Unlink(Engine)
	end

	ActiveTanks[self] = nil

	WireLib.Remove(self)
end

function ENT:OnResized(Size)
	do -- Calculate new empty mass
		local Wall = ACF.FuelArmor * ACF.MmToInch -- Wall thickness in inches
		local Class = self.ClassData
		local _, Area

		if Class.CalcVolume then
			_, Area = Class.CalcVolume(Size, Wall)
		else -- Default to finding surface area/volume based off physics object instead
			local PhysObj = self:GetPhysicsObject()
			Area = PhysObj:GetSurfaceArea()
		end

		local Mass = (Area * Wall) * ACF.InchToCmCu * ACF.SteelDensity -- Total wall volume * cu in to cc * density of steel (kg/cc)

		self.EmptyMass = Mass
	end

	self.HitBoxes = {
		Main = {
			Pos = self:OBBCenter(),
			Scale = Size,
		}
	}
end