-- This parameter determines how long the effects heartbeat loop keeps running after the driver exits
local EFFECTS_GRACE_PERIOD = 0.5


local Effects = {}
Effects.__index = Effects

function Effects.new(chassis, effectsFolder, topModel)
	local self = setmetatable({},Effects)

	self.ignore = topModel

	self.base = chassis:FindFirstChild("FloorPanel")
	self.attachmentContainer = self.base

	local suspensions = {}
	table.insert(suspensions, chassis:FindFirstChild("SuspensionFL"))
	table.insert(suspensions, chassis:FindFirstChild("SuspensionFR"))
	table.insert(suspensions, chassis:FindFirstChild("SuspensionRL"))
	table.insert(suspensions, chassis:FindFirstChild("SuspensionRR"))


	local function createWheelData(wheelPart)
		local attCenter = Instance.new("Attachment")
		attCenter.Name = "EffectsCenter"
		attCenter.Parent = self.attachmentContainer

		local attRight = Instance.new("Attachment")
		attRight.Name = "EffectsR"
		attRight.Parent = self.attachmentContainer

		local attLeft = Instance.new("Attachment")
		attLeft.Name = "EffectsL"
		attLeft.Parent = self.attachmentContainer

		local trail = nil
		local trailPrototype = effectsFolder:FindFirstChild("TireTrail")
		if trailPrototype and trailPrototype:IsA("Trail") then
			trail = trailPrototype:Clone()
			trail.Parent = self.attachmentContainer
			trail.Attachment0 = attLeft
			trail.Attachment1 = attRight
		end
		
		local wheelData = {
			wheel = wheelPart,
			attCenter = attCenter,
			attRight = attRight,
			attLeft = attLeft,
			trail = trail,
			lastContact = 0,
		}
		
		return wheelData
	end

	self.wheels = {}
	for _, suspension in ipairs(suspensions) do
		local wheelPart = suspension:FindFirstChild("Wheel")
		if wheelPart then
			table.insert(self.wheels, createWheelData(wheelPart))
		end
	end
	
	if #self.wheels == 0 then -- probably two-wheeler
		local children = chassis:GetChildren()
		for i = 1, #children do
			if children[i].Name == "Wheel" then
				table.insert(self.wheels, createWheelData(children[i]))
			end
		end
	end

	local engineSound = effectsFolder:FindFirstChild("Engine")
	if engineSound then
		self.engineSound = engineSound:Clone()
		self.engineSound.Parent = chassis.PrimaryPart
		self.engineSoundBaseVolume = engineSound.Volume
	end
	
	local idleSound = effectsFolder:FindFirstChild("Idle")
	if idleSound then
		self.idleSound = idleSound:Clone()
		self.idleSound.Parent = chassis.PrimaryPart
		self.idleSoundBaseVolume = idleSound.Volume
	end
	
	local ignitionSound = effectsFolder:FindFirstChild("EngineStart")
	if ignitionSound then
		self.ignitionSound = ignitionSound:Clone()
		self.ignitionSound.Parent = chassis.PrimaryPart
	end
	
	local stopSound = effectsFolder:FindFirstChild("EngineStop")
	if stopSound then
		self.stopSound = stopSound:Clone()
		self.stopSound.Parent = chassis.PrimaryPart
	end
	
	self.igniting = false
	self.throttle = 0
	self.slideSpeed = 0
	self.gear = 1
	self.disableTime = 0
	self.active = false

	return self
end

function Effects:Enable()
	print("Effects script enabled on", RunService:IsClient() and "Client" or "Server")
	self.active = true
	if #self.wheels > 0 then
		self.disableTime = 0
		if self.heartbeatConn then
			self.heartbeatConn:Disconnect()
		end
		self.heartbeatConn = RunService.Heartbeat:Connect(function(dt)
			self:OnHeartbeat(dt)
		end)
		
		if self.ignitionSound and not self.igniting then
			self.igniting = true
			coroutine.wrap(function()
				if EngineSoundEnabled then
					self.ignitionSound:Play()
					self.ignitionSound.Ended:Wait()
					if self.active then
						self.idleSound:Play()
						self.engineSound:Play()
					end
				end
				self.igniting = false
			end)()
		end
	end
end

function Effects:DisableInternal()
	self.active = false
	if self.heartbeatConn then
		self.heartbeatConn:Disconnect()
	end
	self.heartbeatConn = nil

	-- Disable sounds
	if self.engineSound then self.engineSound:Stop() end
	if self.idleSound then self.idleSound:Stop() end
	if self.stopSound then self.stopSound:Play() end

	if #self.wheels > 0 then
		
		for _,wheelData in ipairs(self.wheels) do
			wheelData.trail.Enabled = false
		end
	end

	self.disableTime = 0
end

function Effects:Disable()
	-- Request effects to be disabled soon (upon driver exiting vehicle)
	if self.disableTime == 0 then
		self.disableTime = tick() + EFFECTS_GRACE_PERIOD
	end
end

local function GearRatio(num)
	return 0.014 * (32/Workspace.Gravity)^0.5 / (0.2 * num + 0.08)
end

local function getEngineSpeed(self, dt)
	return math.abs(self.base.CFrame:vectorToObjectSpace(self.base.Velocity).Z)
end

function Effects:OnHeartbeat(dt)
	if self.disableTime > 0 and tick() > self.disableTime then
		self:DisableInternal()
		return
	end
	
	if TireTrailEnabled then
		for _, wheelData in ipairs(self.wheels) do
			
			local wheel = wheelData.wheel
			
			-- This 'sort-of' calculates whether the wheel is grounded.
			for _, basePart in ipairs(wheel:GetTouchingParts()) do
				if not basePart:IsDescendantOf(self.ignore) then
					wheelData.lastContact = tick()
					break
				end
			end

			if tick() - wheelData.lastContact <= SLIDE_DEBOUNCE then

				local radius = wheel.Size.Y / 2
				local width = wheel.Size.X / 2
				
				local wheelLeftCFrame = wheel.CFrame * CFrame.new(width, 0, 0) - Vector3.new(0, radius - SLIDE_MARK_OFFSET, 0)
				local wheelRightCFrame = wheel.CFrame * CFrame.new(-width, 0, 0) - Vector3.new(0, radius - SLIDE_MARK_OFFSET, 0)
				
				wheelData.attRight.WorldPosition = wheelRightCFrame.p
				wheelData.attLeft.WorldPosition =  wheelLeftCFrame.p
				
				-- RotationalVelocity: Speed at the edge of the wheel from it rotating
				-- HorizontalVelocity: Speed the wheel is actually moving at
				-- SlideSpeed: The speed at which the wheel is sliding relative to its rotational velocity
				local rotationalVelocity = radius * (self.base.CFrame:VectorToObjectSpace(wheel.RotVelocity)).X
				local horizontalVelocity = self.base.CFrame:VectorToObjectSpace(wheel.Velocity).Z
				local slideSpeed = math.abs(rotationalVelocity - horizontalVelocity)
				
				local slipValue = slideSpeed / math.abs(rotationalVelocity)
				local sliding = slipValue >= SLIDE_THRESHOLD

				wheelData.trail.Enabled = sliding

			else
				wheelData.trail.Enabled = false
			end
			
		end
	end

	--Engine Sounds
	if EngineSoundEnabled and not self.igniting then
		if self.engineSound then
	
			local engineSpeed = getEngineSpeed(self, dt)
			local rawPitch = engineSpeed * GearRatio(self.gear + PITCH_GEAR_MODIFIER)
			if rawPitch > PITCH_SHIFT_UP then
				self.gear = self.gear + 1
			elseif rawPitch < PITCH_SHIFT_DOWN and self.gear > 1 then
				self.gear = self.gear - 1
			end
			
			local targetPitch = rawPitch * PITCH_MODIFIER
			local pitchMaxDelta = PITCH_MAX_DELTA * dt
			local pitch = self.engineSound.PlaybackSpeed + math.clamp(targetPitch - self.engineSound.PlaybackSpeed, -pitchMaxDelta, pitchMaxDelta)
			local engineVolume = math.min(targetPitch >= 0.6 and 1 or targetPitch / 0.6, 1)
			
			self.engineSound.PlaybackSpeed = math.max(pitch, 0.5)
			self.engineSound.Volume = self.engineSoundBaseVolume * engineVolume
			self.idleSound.Volume = self.idleSoundBaseVolume * (1 - engineVolume)
			
		end
	end

end

return Effects