local ai = {}

AI_COMP_OVERRIDE = false
AI_THROTTLE_LIMIT = 0.8
AI_LEVEL = 0.9
AI_AGGRESSION = 0.25
-- Constantes para el desgaste de neumáticos
local AI_AVG_TYRE_LIFE = 80 -- valor por defecto para la vida promedio de los neumáticos
local AI_SINGLE_TYRE_LIFE = 75 -- valor por defecto para la vida de un único neumático

--- Returns whether driver's average tyre life is below
--- the limit or not
---@param driver Driver
---@return bool
local function avgTyreWearBelowLimit(driver)
    local avgTyreLimit = 1 - (AI_AVG_TYRE_LIFE + driver.aiTyreAvgRandom) / 100
    local avgTyreWear = (
        driver.car.wheels[0].tyreWear
        + driver.car.wheels[1].tyreWear
        + driver.car.wheels[2].tyreWear
        + driver.car.wheels[3].tyreWear
    ) / 4
    return avgTyreWear > avgTyreLimit
end

---@alias ai.QualifyLap
---| `ai.QualifyLap.OutLap` @Value: 0.
---| `ai.QualifyLap.FlyingLap` @Value: 1.
---| `ai.QualifyLap.InLap` @Value: 2.
ai.QualifyLap = { OutLap = 0, FlyingLap = 1, InLap = 2 }

--- Returns whether one of a driver's tyre life is below
--- the limit or not
---@param driver Driver
---@return bool
local function singleTyreWearBelowLimit(driver)
    local singleTyreLimit = 1 - (AI_SINGLE_TYRE_LIFE + driver.aiTyreSingleRandom) / 100

	if
		driver.car.wheels[0].tyreWear > singleTyreLimit
		or driver.car.wheels[1].tyreWear > singleTyreLimit
		or driver.car.wheels[2].tyreWear > singleTyreLimit
		or driver.car.wheels[3].tyreWear > singleTyreLimit
	then
		return true
	else
		return false
	end
end

local function getNextTyreCompound(driver)
	local compoundsAvailable = driver.tyreCompoundsAvailable
	local compoundNext = driver.car.compoundIndex

	math.randomseed(os.clock() * driver.index)
	math.random()

	if not driver.tyreCompoundChange then
		table.removeItem(compoundsAvailable, driver.car.compoundIndex)
		compoundNext = compoundsAvailable[math.random(1, #compoundsAvailable)]
	else
		compoundNext = compoundsAvailable[math.random(1, #compoundsAvailable)]
	end

	return compoundNext
end

--- Force AI driver to drive to the pits
--- @param driver Driver
local function triggerPitStopRequest(driver, trigger)
	if ac.getPatchVersionCode() >= 2278 then
		physics.setAIPitStopRequest(driver.index, trigger)
	else
		if trigger and not driver.aiPitting then
			driver.aiPrePitFuel = driver.car.fuel
			physics.setCarFuel(driver.index, 0.1)
			driver.aiPitCall = true
		end
	end

	driver.aiPitting = trigger
	if trigger then
		driver.tyreCompoundNext = getNextTyreCompound(driver)
	end
end

--- Determine if going to the pits is advantageous or not
--- @param driver Driver
--- @param forced boolean
local function pitStrategyCall(driver, forced)
	local carAhead = DRIVERS[driver.carAhead]
	local trigger = true
	local lapsTotal = ac.getSession(ac.getSim().currentSessionIndex).laps
	local lapsRemaining = lapsTotal - driver.lapsCompleted

	if not forced then
		if lapsRemaining <= 7 or (driver.carAheadDelta < 1 and carAhead.aiPitting) then
			trigger = false
		end
	end

	triggerPitStopRequest(driver, trigger)
end
--- Occurs when a driver is in the pit
---@param driver Driver
local function pitstop(raceRules, driver)
	if driver.aiPitting then
		if not driver.tyreCompoundChange and driver.tyreCompoundNext ~= driver.tyreCompoundStart then
			driver.tyreCompoundChange = true
		end

		driver.tyreStints[#driver.tyreStints] = {
			ac.getTyresName(driver.index, driver.car.compoundIndex),
			driver.tyreLaps,
		}

		table.insert(driver.tyreStints, {
			ac.getTyresName(driver.index, driver.tyreCompoundNext),
			0,
		})

		driver.tyreLaps = 0

		if ac.getPatchVersionCode() >= 2278 then
			physics.setAITyres(driver.index, driver.tyreCompoundNext)
		end
	end

	if raceRules.RACE_REFUELING ~= 1 then
		physics.setCarFuel(driver.index, driver.aiPrePitFuel)
	end

	driver.aiPitting = false
end

--- Determines when an AI driver should pit for new tyres
--- @param driver Driver
function ai.pitNewTyres(raceRules, driver)
	if not driver.car.isInPitlane and not driver.aiPitting then
		if singleTyreWearBelowLimit(driver) then
			pitStrategyCall(driver, true)
		elseif avgTyreWearBelowLimit(driver) then
			pitStrategyCall(driver, false)
		end
	else
		if driver.car.isInPit then
			pitstop(raceRules, driver)
		end

		if driver.aiPitCall then
			physics.setCarFuel(driver.index, driver.aiPrePitFuel)
			driver.aiPitCall = false
		end
	end
end

local function moveOffRacingLine(driver)
	local delta = driver.carAheadDelta
	local driverAhead = DRIVERS[driver.carAhead]
	local splineSideLeft = ac.getTrackAISplineSides(driverAhead.splinePosition).x
	local splineSideRight = ac.getTrackAISplineSides(driverAhead.splinePosition).y
	local offset = splineSideLeft > splineSideLeft and -3 or 3

	if not driverAhead.flyingLap then
		if delta < 2 and delta > 0.1 then
			if driverAhead.car.isInPitlane then
				physics.setAITopSpeed(driverAhead.index, 0)
			else
				if ac.getTrackUpcomingTurn(driverAhead.index).x > 100 then
					physics.setAISplineOffset(driverAhead.index, math.lerp(0, offset, driverAhead.aiSplineOffset))
					physics.setAISplineOffset(driver.index, math.lerp(0, -offset, driver.aiSplineOffset))
					physics.setAITopSpeed(driverAhead.index, 200)
					driverAhead.aiMoveAside = true
					driverAhead.aiSpeedUp = false
				else
					driverAhead.aiSplineOffset = math.lerp(offset, 0, driverAhead.aiSplineOffset)
					driver.aiSplineOffset = math.lerp(-offset, 0, driver.aiSplineOffset)
					physics.setAISplineOffset(driverAhead.index, driverAhead.aiSplineOffset)
					physics.setAISplineOffset(driver.index, driver.aiSplineOffset)
					physics.setAITopSpeed(driverAhead.index, 1e9)
					driverAhead.aiMoveAside = false
					driverAhead.aiSpeedUp = true
				end
			end
		else
			driverAhead.aiSplineOffset = math.lerp(offset, 0, driverAhead.aiSplineOffset)
			driver.aiSplineOffset = math.lerp(-offset, 0, driver.aiSplineOffset)
			physics.setAISplineOffset(driverAhead.index, driverAhead.aiSplineOffset)
			physics.setAISplineOffset(driver.index, driver.aiSplineOffset)
			driverAhead.aiMoveAside = false
			driverAhead.aiSpeedUp = false
		end
	end
end

function ai.qualifying(driver)
	if driver.car.isInPitlane then
		physics.allowCarDRS(driver.index, true)
		if driver.car.isInPit then
			local qualiRunFuel = driver.fuelPerLap * 3.5
			physics.setCarFuel(driver.index, qualiRunFuel)
		end
		driver.inLap = false
		driver.flyingLap = false
		driver.outLap = true
	else
		if driver.flyingLap then
			moveOffRacingLine(driver)
			if ac.getTrackUpcomingTurn(driver.index).x > 200 then
				physics.setAIAggression(driver.index, 1)
			else
				physics.setAIAggression(driver.index, 0.25)
			end

			if driver.car.lapCount >= driver.inLapCount then
				driver.outLap = false
				driver.flyingLap = false
				driver.inLap = true
			end
		elseif driver.outLap then
			if driver.car.splinePosition <= 0.8 then
				if not driver.aiMoveAside and not driver.aiSpeedUp then
					physics.setAITopSpeed(driver.index, 250)
					physics.setAILevel(driver.index, 0.8)
					physics.setAIAggression(driver.index, 1)
					physics.allowCarDRS(driver.index, true)

					if driver.carAheadDelta < 5 then
						physics.setAITopSpeed(driver.index, 200)
					end
				elseif driver.aiSpeedUp then
					moveOffRacingLine(driver)
				end
			else
				physics.setAITopSpeed(driver.index, 1e9)
				physics.setAILevel(driver.index, 1)
				physics.setAIAggression(driver.index, 1)
				physics.allowCarDRS(driver.index, false)
				driver.outLap = false
				driver.flyingLap = true
				driver.inLapCount = driver.car.lapCount + 2
			end
		elseif driver.inLap then
			if not driver.aiMoveAside and not driver.aiSpeedUp then
				physics.setAITopSpeed(driver.index, 200)
				physics.setAILevel(driver.index, 0.65)
				physics.setAIAggression(driver.index, 0)
			elseif driver.aiSpeedUp then
				moveOffRacingLine(driver)
			end
		end
	end
end

local function altAIAggression(driver, upcomingTurn)
	local delta = driver.carAheadDelta

	if upcomingTurn.x > 150 and delta < 0.5 then
		return 1
	elseif delta < 1 then
		return driver.aiAggression
	else
		return 0
	end
end

local function altAIBrakeHint(driver, upcomingTurn)
	local delta = driver.carAheadDelta
	local brakeHint = driver.aiBaseBrakeHint

	if upcomingTurn.x > 150 and delta < 0.15 then
		return brakeHint + (brakeHint * 0.05)
	elseif upcomingTurn.x > 150 and delta < 0.4 then
		return brakeHint + (brakeHint * 0.10)
	elseif not (upcomingTurn.x > 0) and delta < 0.75 then
		return brakeHint
	else
		return brakeHint - (brakeHint * 0.05)
	end
end

local function altAIThrottleLimit(driver, upcomingTurn)
	if upcomingTurn.x > 50 then
		return math.applyLag(driver.aiThrottleLimit, 1, 0.99, ac.getScriptDeltaT())
	else
		return math.applyLag(driver.aiThrottleLimit, driver.aiThrottleLimitBase, 0.96, ac.getScriptDeltaT())
	end
end

--- Variable aggression function for AI drivers
--- @param driver Driver
function ai.alternateLevel(driver)
	local upcomingTurn = ac.getTrackUpcomingTurn(driver.index)

	local aiAggression = altAIAggression(driver, upcomingTurn)
	driver.aiBrakeHint = altAIBrakeHint(driver, upcomingTurn)
	driver.aiThrottleLimit = altAIThrottleLimit(driver, upcomingTurn)

	physics.setAIAggression(driver.index, aiAggression)
	physics.setAIThrottleLimit(driver.index, driver.aiThrottleLimit)

	if ac.getPatchVersionCode() >= 2278 then
		physics.setAIBrakeHint(driver.index, driver.aiBrakeHint)
	end
end

local function mgukBuild(driver)
	if driver.aiMgukDelivery ~= 1 then
		driver.aiMgukDelivery = 1
		physics.setMGUKDelivery(driver.index, driver.aiMgukDelivery)
	end

	if driver.aiMgukRecovery ~= 10 then
		driver.aiMgukRecovery = 10
		physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
	end
end

local function mgukLow(driver)
	if driver.aiMgukDelivery ~= 2 then
		driver.aiMgukDelivery = 2
		physics.setMGUKDelivery(driver.index, driver.aiMgukDelivery)
	end

	-- driver.car.kersCurrentKJ
	if driver.car.kersCharge > 0.8 then
		if driver.aiMgukRecovery ~= 0 then
			driver.aiMgukRecovery = 0
			physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
		end
	elseif driver.car.kersCharge > 0.15 then
		if driver.aiMgukRecovery ~= 7 then
			driver.aiMgukRecovery = 7
			physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
		end
	else
		mgukBuild(driver)
	end
end

local function mgukBalanced(driver)
	if driver.aiMgukDelivery ~= 3 then
		driver.aiMgukDelivery = 3
		physics.setMGUKDelivery(driver.index, driver.aiMgukDelivery)
	end

	-- driver.car.kersCurrentKJ
	if driver.car.kersCharge > 0.8 then
		if driver.aiMgukRecovery ~= 0 then
			driver.aiMgukRecovery = 0
			physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
		end
	elseif driver.car.kersCharge > 0.25 then
		if driver.aiMgukRecovery ~= 7 then
			driver.aiMgukRecovery = 7
			physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
		end
	else
		mgukLow(driver)
	end
end

local function mgukHigh(driver)
	if driver.aiMgukDelivery ~= 4 then
		driver.aiMgukDelivery = 4
		physics.setMGUKDelivery(driver.index, driver.aiMgukDelivery)
	end

	-- driver.car.kersCurrentKJ
	if driver.car.kersCharge > 0.8 then
		if driver.aiMgukRecovery ~= 0 then
			driver.aiMgukRecovery = 0
			physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
		end
	elseif driver.car.kersCharge > 0.45 then
		if driver.aiMgukRecovery ~= 7 then
			driver.aiMgukRecovery = 7
			physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
		end
	else
		mgukBalanced(driver)
	end
end

local function mgukAttack(driver)
	if driver.aiMgukDelivery ~= 5 then
		driver.aiMgukDelivery = 5
		physics.setMGUKDelivery(driver.index, driver.aiMgukDelivery)
	end

	if driver.aiMgukRecovery ~= 0 then
		driver.aiMgukRecovery = 0
		physics.setMGUKRecovery(driver.index, driver.aiMgukRecovery)
	end
end

function ai.mgukController(driver)
	if driver.carAheadDelta < 0.5 and driver.car.kersCharge > 0.25 then
		mgukAttack(driver)
	elseif driver.carAheadDelta < 1.5 then
		mgukHigh(driver)
	else
		mgukBalanced(driver)
	end
end

function ai.controller(raceRules, aiRules, driver)
	if aiRules.AI_FORCE_PIT_TYRES == 1 then
		ai.pitNewTyres(raceRules, driver)
	end
	if aiRules.AI_ALTERNATE_LEVEL == 1 then
		ai.alternateLevel(driver)
	end
	if aiRules.AI_MGUK_CONTROL == 1 and ac.getPatchVersionCode() >= 2555 then
		ai.mgukController(driver)
	end
end

return ai
