require "luautils"

require "rLib.Client"
require "rSemiTruck/ArmorSync"
pcall(function() require "MSW_MassUtil" end)
rSemiTruck = rSemiTruck or {}
rSemiTruck.Event = {}

local ArmorSync = rSemiTruck.ArmorSync

local function IsSemiTruckAny(vehicle)
	assert(rLib.arg(vehicle, "BaseVehicle"))

	local vehicleName = vehicle:getScript():getFullName()

	return vehicleName == "Base.SemiTruck" or vehicleName == "Base.SemiTruckLite" or vehicleName == "Base.SemiTruckBox"
end

local function IsSemiTruckOrSemiTruckLite(vehicle)
	assert(rLib.arg(vehicle, "BaseVehicle"))

	local vehicleName = vehicle:getScript():getFullName()

	return vehicleName == "Base.SemiTruck" or vehicleName == "Base.SemiTruckLite"
end

local function IsSemiTrailer(vehicle)
	assert(rLib.arg(vehicle, "BaseVehicle"))

	local vehicleName = vehicle:getScript():getFullName()

	return vehicleName == "Base.SemiTrailerVan"
		or vehicleName == "Base.SemiTrailerVan_mil"
		or vehicleName == "Base.SemiTrailerVanCattle"
		or vehicleName == "Base.SemiTrailerCistern"
end

local function SupportsTrailerBatterySync(vehicle)
	assert(rLib.arg(vehicle, "BaseVehicle"))

	return vehicle:getPartById("Battery") ~= nil and vehicle:getPartById("Battery1") == nil
end

local ENGINE_BULLBAR_CACHE_KEY = "rSemiTruck.EngineBullbarCache"
local ENGINE_BULLBAR_LATCH_KEY = "rSemiTruck.EngineBullbarLatched"
local ENGINE_BULLBAR_LAST_ABSORB_KEY = "rSemiTruck.EngineBullbarLastAbsorb"
local TRAILER_PHYSICS_DISABLED_KEY = "rSemiTruck.TrailerPhysicsDisabled"
local ARMOR_REPAIR_CACHE_KEY = "rSemiTruck.ArmorRepairCache"
local ARMOR_GUARD_WEAR_BUFFER_KEY = "rSemiTruck.ArmorGuardWearBuffer"
local ARMOR_GUARD_WEAR_RATE = 0.10

local IsTrailerPhysicsDisabled
local ApplyTrailerPhysicsState

local function clampPartCondition(condition)
	return ArmorSync.clampCondition(condition)
end

local function requestArmorRepair(player, vehicle, part, condition)
	return ArmorSync.dispatchRepair(player, vehicle, part, condition, "W900", "applyArmorRepair")
end

local function getArmorRepairCache(vehicle)
	local modData = vehicle:getModData()
	modData[ARMOR_REPAIR_CACHE_KEY] = modData[ARMOR_REPAIR_CACHE_KEY] or {}
	return modData[ARMOR_REPAIR_CACHE_KEY]
end

local function clearArmorRepairState(vehicle, part)
	if not vehicle or not part then
		return
	end

	local cache = vehicle:getModData()[ARMOR_REPAIR_CACHE_KEY]
	if cache then
		cache[part:getId()] = nil
	end
end

local function getArmorGuardWearBuffer(vehicle)
	local modData = vehicle:getModData()
	modData[ARMOR_GUARD_WEAR_BUFFER_KEY] = modData[ARMOR_GUARD_WEAR_BUFFER_KEY] or {}
	return modData[ARMOR_GUARD_WEAR_BUFFER_KEY]
end

local function consumeArmorGuardWear(vehicle, guard, repairAmount)
	if not vehicle or not guard or repairAmount <= 0 then
		return 0
	end

	local wearBuffer = getArmorGuardWearBuffer(vehicle)
	local guardId = guard:getId()
	local accumulatedWear = (tonumber(wearBuffer[guardId]) or 0) + (repairAmount * ARMOR_GUARD_WEAR_RATE)
	local appliedWear = math.floor(accumulatedWear)

	if appliedWear > 0 then
		wearBuffer[guardId] = accumulatedWear - appliedWear
	else
		wearBuffer[guardId] = accumulatedWear
	end

	return appliedWear
end

local function getArmorRepairTarget(vehicle, part, targetCondition)
	if not vehicle or not part or targetCondition == nil then
		return nil
	end

	local currentCondition = clampPartCondition(part:getCondition())
	local configuredTarget = clampPartCondition(targetCondition)
	local cache = getArmorRepairCache(vehicle)
	local cached = cache[part:getId()]

	if not cached then
		cache[part:getId()] = {
			lastCondition = currentCondition,
			maxRepairCondition = math.min(currentCondition, configuredTarget),
		}
		return nil
	end

	local lastCondition = clampPartCondition(cached.lastCondition or cached.currentCondition or currentCondition)
	local maxRepairCondition = clampPartCondition(cached.maxRepairCondition or cached.targetCondition or currentCondition)
	maxRepairCondition = math.min(maxRepairCondition, configuredTarget)

	if currentCondition > maxRepairCondition then
		maxRepairCondition = math.min(currentCondition, configuredTarget)
	end

	cache[part:getId()] = {
		lastCondition = currentCondition,
		maxRepairCondition = maxRepairCondition,
	}

	if currentCondition >= maxRepairCondition or currentCondition == lastCondition then
		return nil
	end

	return maxRepairCondition
end

local function clearEngineBullbarState(vehicle)
	local modData = vehicle:getModData()
	modData[ENGINE_BULLBAR_CACHE_KEY] = nil
	modData[ENGINE_BULLBAR_LATCH_KEY] = nil
	modData[ENGINE_BULLBAR_LAST_ABSORB_KEY] = nil
end

local function getEngineBullbarCache(vehicle)
	local cached = vehicle:getModData()[ENGINE_BULLBAR_CACHE_KEY]
	if cached == nil then
		return nil
	end

	return clampPartCondition(cached)
end

local function setEngineBullbarCache(vehicle, condition)
	vehicle:getModData()[ENGINE_BULLBAR_CACHE_KEY] = clampPartCondition(condition)
end

local function getArmorGuard(vehicle, armor)
	if type(armor) ~= "table" or type(armor.needs) ~= "table" or not armor.needs.part then
		return nil
	end

	return vehicle:getPartById(armor.needs.part)
end

local function isArmorGuardActive(vehicle, armor)
	local guard = getArmorGuard(vehicle, armor)
	if not guard or not guard:getInventoryItem() then
		return false, nil
	end

	local requiredCondition = type(armor.needs) == "table" and tonumber(armor.needs.condition) or nil
	if requiredCondition and guard:getCondition() < requiredCondition then
		return false, guard
	end

	return true, guard
end

local function damageArmorGuard(player, vehicle, armor, repairAmount)
	if repairAmount <= 0 then
		return
	end

	local ok, guard = isArmorGuardActive(vehicle, armor)
	if not ok or not guard then
		return
	end

	local appliedWear = consumeArmorGuardWear(vehicle, guard, repairAmount)
	if appliedWear > 0 then
		local guardCondition = math.max(guard:getCondition() - appliedWear, 0)
		requestArmorRepair(player, vehicle, guard, guardCondition)
	end
end

local function shouldProtectEngineWithBullbar(vehicle, armor)
	local guardActive = isArmorGuardActive(vehicle, armor)
	if not guardActive then
		clearEngineBullbarState(vehicle)
		return false
	end

	local modData = vehicle:getModData()
	if modData[ENGINE_BULLBAR_LATCH_KEY] then
		return true
	end

	local engineDoor = vehicle:getPartById("EngineDoor")
	if engineDoor and (not engineDoor:getInventoryItem() or engineDoor:getCondition() <= 0) then
		modData[ENGINE_BULLBAR_LATCH_KEY] = true
		return true
	end

	return false
end

function rSemiTruck.Event.EngineBullbarCheck(player, vehicle, part, armor, action)
	assert(rLib.arg(player, "IsoPlayer"))
	assert(rLib.arg(vehicle, "BaseVehicle"))
	assert(rLib.arg(part, "VehiclePart"))
	assert(rLib.arg(armor, "table"))
	assert(rLib.arg(action, "string"))

	if part:getId() ~= "Engine" or not shouldProtectEngineWithBullbar(vehicle, armor) then
		return false
	end

	local modData = vehicle:getModData()
	local currentCondition = clampPartCondition(part:getCondition())
	local cachedCondition = getEngineBullbarCache(vehicle)

	if not cachedCondition then
		cachedCondition = currentCondition
		setEngineBullbarCache(vehicle, cachedCondition)
	end

	if currentCondition > cachedCondition then
		setEngineBullbarCache(vehicle, currentCondition)
		modData[ENGINE_BULLBAR_LAST_ABSORB_KEY] = nil
	elseif currentCondition < cachedCondition then
		local lastAbsorbed = modData[ENGINE_BULLBAR_LAST_ABSORB_KEY]
		if tonumber(lastAbsorbed) ~= currentCondition then
			requestArmorRepair(player, vehicle, part, cachedCondition)
			damageArmorGuard(player, vehicle, armor, cachedCondition - currentCondition)
			modData[ENGINE_BULLBAR_LAST_ABSORB_KEY] = currentCondition
		end
	else
		modData[ENGINE_BULLBAR_LAST_ABSORB_KEY] = nil
	end

	return true
end

function rSemiTruck.Event.EngineBullbarRepair(player, vehicle, part, armor, action)
	assert(rLib.arg(player, "IsoPlayer"))
	assert(rLib.arg(vehicle, "BaseVehicle"))
	assert(rLib.arg(part, "VehiclePart"))
	assert(rLib.arg(armor, "table"))
	assert(rLib.arg(action, "string"))

	return false
end

function rSemiTruck.Event.DoorRearBullbarCheck(player, vehicle, part, armor, action)
	assert(rLib.arg(player, "IsoPlayer"))
	assert(rLib.arg(vehicle, "BaseVehicle"))
	assert(rLib.arg(part, "VehiclePart"))
	assert(rLib.arg(armor, "table"))
	assert(rLib.arg(action, "string"))

	if part:getId() ~= "DoorRear" then
		return false
	end

	return isArmorGuardActive(vehicle, armor)
end

function rSemiTruck.Event.DoorRearBullbarRepair(player, vehicle, part, armor, action)
	assert(rLib.arg(player, "IsoPlayer"))
	assert(rLib.arg(vehicle, "BaseVehicle"))
	assert(rLib.arg(part, "VehiclePart"))
	assert(rLib.arg(armor, "table"))
	assert(rLib.arg(action, "string"))

	if part:getId() ~= "DoorRear" then
		return false
	end

	return rSemiTruck.Event.PostAutoRepair(player, vehicle, part, armor)
end

rSemiTruck.Event.TruckBedBullbarCheck = rSemiTruck.Event.DoorRearBullbarCheck
rSemiTruck.Event.TruckBedBullbarRepair = rSemiTruck.Event.DoorRearBullbarRepair

function rSemiTruck.Event.PostAutoRepair(player, vehicle, part, armor) -- called directly by rLib --
	assert(rLib.arg(player, "IsoPlayer"))
	assert(rLib.arg(vehicle, "BaseVehicle"))
	assert(rLib.arg(part, "VehiclePart"))
	assert(rLib.arg(armor, "table"))

	-- rLib.dprint(string.format("[rSemiTruck] PostAutoRepair : %s : %s", vehicle:getScript():getFullName(), part:getId()))

	local targetCondition = tonumber(armor.condition)
	if not targetCondition then
		return false
	end

	local repairTarget = getArmorRepairTarget(vehicle, part, targetCondition)
	if not repairTarget then
		return false
	end

	if type(armor.needs) == "table" and armor.needs.part then
		damageArmorGuard(player, vehicle, armor, repairTarget - part:getCondition())
	end

	requestArmorRepair(player, vehicle, part, repairTarget)

	return false
end

function rSemiTruck.Event.ToggleHeadlights(player, vehicle)
	assert(rLib.arg(player, "IsoPlayer"))
	assert(rLib.arg(vehicle, "BaseVehicle"))

	--[[ if not IsSemiTruckOrSemiTruckLite(vehicle) then
		return
	end --]]

	local trailer = vehicle:getVehicleTowing()
	if not trailer or not IsSemiTrailer(trailer) then
		return
	end

	rLib.Commands.SendToServer(player, "SetVehicleHeadlights", {vehicleId = trailer:getId(), set = not vehicle:getHeadlightsOn()}, true)
end

function rSemiTruck.Event.AttachDetachVehicle(player, vehicleA, vehicleB)
	assert(rLib.arg(player, "IsoPlayer"))
	assert(rLib.arg(vehicleA, "BaseVehicle"))

	if not rLib.arg(vehicleB, "BaseVehicle") then
		return
	end

	if --[[ not IsSemiTruckOrSemiTruckLite(vehicleA) or --]] not IsSemiTrailer(vehicleB) then
		return
	end

	if rLib.Events.Current == "Vehicle.DetachVehicle" then
		rLib.Commands.SendToServer(player, "SetVehicleBattery",
		{
			vehicleId = vehicleB:getId(),
			battery = 0
		}, true)
	elseif IsSemiTruckAny(vehicleA) and SupportsTrailerBatterySync(vehicleA) then
		local batteryVar = vehicleA:getBatteryCharge()
		if batteryVar > 0 then
			rLib.Commands.SendToServer(player, "SetVehicleBattery",
			{
				vehicleId = vehicleB:getId(),
				battery = batteryVar
			}, true)
		end
	end

	rLib.Commands.SendToServer(player, "SetVehicleHeadlights",
	{
		vehicleId = vehicleB:getId(),
		set = rLib.Events.Current == "Vehicle.AttachVehicle" and vehicleA:getHeadlightsOn() or false
	}, true)
end

function rSemiTruck.Event.PlayerUpdate(player)
	local vehicle = player:getVehicle() or player:getNearVehicle()
	if not vehicle then
		return
	end

	if vehicle:getVehicleTowing() then
		vehicle = vehicle:getVehicleTowing()
	end

	-- process semi-trailer only --
	local attachment = vehicle:getTowAttachmentOther()
	if not attachment or string.lower(attachment) ~= "trailertruck" --[[or attachment ~= "trailer"]] then
		return
	end

	local partTrunk = vehicle:getPartById("TruckBed")
	if not partTrunk then
		return
	end

	-- PayloadDamping is the authoritative mass system.
	-- If the shared mass utility is present, vehicles outside the filter should not be dampened
	-- by this legacy quarter-payload fallback.
	if MSW_MassUtil then
		return
	end

	local partsContainersMass = 0
	local partsInventoryMass = 0
	for p=0, vehicle:getPartCount()-1 do
		local part = vehicle:getPartByIndex(p)
		local item = part:getItemContainer()
		if item then
			partsContainersMass = partsContainersMass + item:getCapacityWeight()
		end

		item = part:getInventoryItem()
		if item then
			partsInventoryMass = partsInventoryMass + item:getWeight()
		end
	end

	local wantedMass = vehicle:getInitialMass() + partsInventoryMass + (partsContainersMass / 4)
	vehicle:setMass(math.floor(wantedMass))
end

function rSemiTruck.Event.EnterVehicle(player)
	if not rLib.modversion then -- modversion() was added in v1.2 --
		luautils.okModal("Your rLib is older than Lexx's mouse")
		return
	end

	local r = rLib.modversion("RotatorsLib") or rLib.modversion("RotatorsLib_DEV")
	if not r or r < 1.2 then
		luautils.okModal("Your rLib is bad and you should feel bad")
	end
end

IsTrailerPhysicsDisabled = function(vehicle)
	if not vehicle then
		return false
	end

	return vehicle:getModData()[TRAILER_PHYSICS_DISABLED_KEY] == true
end

ApplyTrailerPhysicsState = function(vehicle, disabled)
	if not vehicle then
		return
	end

	vehicle:getModData()[TRAILER_PHYSICS_DISABLED_KEY] = disabled and true or false
	if vehicle.setPhysicsActive then
		pcall(vehicle.setPhysicsActive, vehicle, not disabled)
	end
	if vehicle.transmitModData then
		pcall(vehicle.transmitModData, vehicle)
	end
end

rLib.Events.On("Vehicle.ToggleHeadlights", rSemiTruck.Event.ToggleHeadlights)
rLib.Events.On("Vehicle.AttachVehicle", rSemiTruck.Event.AttachDetachVehicle)
rLib.Events.On("Vehicle.DetachVehicle", rSemiTruck.Event.AttachDetachVehicle)
Events.OnPlayerUpdate.Add(rSemiTruck.Event.PlayerUpdate)
--Events.OnEnterVehicle.Add(rSemiTruck.Event.EnterVehicle)

--[[

TODO/wait for fix

local ISVM_doTowingMenu = ISVehicleMenu.doTowingMenu
function ISVehicleMenu.doTowingMenu(player, trailer, radial, ...)
	ISVM_doTowingMenu(player, trailer, radial, ...)

	local vehicleName = trailer:getScript():getFullName()
	if vehicleName ~= "Base.SemiTruckBox" and vehicleName ~= "Base.SemiTrailerVan" then
		return
	end

	if radial and radial.slices then
		for idx,slice in ipairs(radial.slices) do
			if slice.text == getText("IGUI_OpenTrunk") or slice.text == getText("IGUI_CloseTrunk") then
				return
			end
		end
	end

	local part = trailer:getPartById("DoorRear")
	if part and part:getInventoryItem() then
		local text = part:getDoor():isOpen() and "IGUI_CloseTrunk" or "IGUI_OpenTrunk"
		local func = part:getDoor():isOpen() and ISVehicleMenu.onCloseDoor or ISVehicleMenu.onOpenDoor
		radial:addSlice(getText(text), getTexture("media/ui/vehicles/vehicle_exit.png"), func, player, part)
	end
end
]]--

--[[
	Enables vehicle overlay editor for selected vehicle(s)
	Can be temporary enabled for any near vehicle by using debug mode console command: rLib.UI.VehicleOverlayEditor.Try()
	RotatorsLib : https://steamcommunity.com/sharedfiles/filedetails/?id=2732594572
]]--

if getDebug() and rLib and rLib.Events.Exists("Vehicle.MechanicsSetVisible") then
	local function EnableVehicleOverlayEditor(ui, uiVisible)
		if uiVisible then
			local vehicleName = ui.vehicle:getScript():getFullName()
			if vehicleName == "Base.SemiTruck" or vehicleName == "Base.SemiTruckLite" or vehicleName == "Base.SemiTruckBox" then
				rLib.UI.VehicleOverlayEditor.Enable(ui)
			end
		end
	end
	rLib.Events.On("Vehicle.MechanicsSetVisible", EnableVehicleOverlayEditor)
end
