PRINT_PREFIX = "[UseYourTyres] - "

UseYourTyres = {};
UseYourTyres.modDir = g_currentModDirectory
UseYourTyres.REPLACE_PRICE = 1000
UseYourTyres.USED_MAX_M = 240000
UseYourTyres.hasGui = g_gui ~= nil and WorkshopScreen ~= nil and ButtonElement ~= nil and BitmapElement ~= nil

source(g_currentModDirectory .. "src/useYourTyresReplaceEvent.lua")
source(g_currentModDirectory .. "src/useYourTyresDistanceMultiplierEvent.lua")
source(g_currentModDirectory .. "src/useYourTyresSettings.lua")

UseYourTyres.tyreDistanceMultiplier = UseYourTyresSettings.tyreDistanceMultipliers[2]

function UseYourTyres.injWheelPostLoad(wheel)
	local hasAnyTire = false
	for _, visualWheel in ipairs(wheel.visualWheels) do
		for _, visualPart in ipairs(visualWheel.visualParts) do
			if visualPart:isa(WheelVisualPartTire) then
				visualPart.uytEnabled = true
				visualPart.uytSupportsVisualWear = false
				hasAnyTire = true
			end
		end

		wheel.vehicle.uytHasTyres = wheel.vehicle.uytHasTyres or hasAnyTire
	end

	return true
end
Wheel.postLoad = Utils.appendedFunction(Wheel.postLoad, UseYourTyres.injWheelPostLoad)


function UseYourTyres.injOnReadStream(vehicle, streamId, connection)
	if connection.isServer then
		vehicle.uytHasTyres = streamReadBool(streamId)
		vehicle.uytUnsupported = streamReadBool(streamId)
		if vehicle.uytHasTyres == true then
			local spec_wheels = vehicle.spec_wheels
			for _,  wheel in ipairs(spec_wheels.wheels) do
				local v0 = streamReadFloat32(streamId)
				if v0 ~= -1 then
					wheel.uytReplacePrice = v0
				end
				local v1 = streamReadFloat32(streamId)
				if v1 ~= -1 then
					wheel.uytTravelledDist = v1
				end
				-- print(PRINT_PREFIX .. "injOnReadStream just read, replacePrice: " .. v0  .. "; travelledDist: " .. v1 .. "m!")
				
				for _, visualWheel in ipairs(wheel.visualWheels) do
					for _, visualPart in ipairs(visualWheel.visualParts) do
						if visualPart:isa(WheelVisualPartTire) then
							local v2 = streamReadFloat32(streamId)
							local v3 = streamReadFloat32(streamId)
							local v4 = streamReadFloat32(streamId)
							local v5 = streamReadFloat32(streamId)
							if v2 ~= -1 and v3 ~= -1 and v4 ~= -1 and v5 ~= -1 then
								visualPart.uytMaxUsage = v2
								visualPart.uytWidth = v3
								visualPart.uytMaxRadius = v4
								visualPart.uytMaxPhysRadius = v5
								-- print(PRINT_PREFIX .. "injOnReadStream just read, radius: " .. v2 .. "; maxUsage: " .. v1)
							end
						end
					end
				end
			end
		end
	end
end
Wheels.onReadStream = Utils.appendedFunction(Wheels.onReadStream, UseYourTyres.injOnReadStream)


function UseYourTyres.injOnWriteStream(vehicle, streamId, connection)
	if not connection.isServer then
		streamWriteBool(streamId, vehicle.uytHasTyres or false)
		streamWriteBool(streamId, vehicle.uytUnsupported or false)
		if vehicle.uytHasTyres == true then
			local spec_wheels = vehicle.spec_wheels
			for _,  wheel in ipairs(spec_wheels.wheels) do
				streamWriteFloat32(streamId, wheel.uytReplacePrice or -1)
				streamWriteFloat32(streamId, wheel.uytTravelledDist or -1)
				-- print(PRINT_PREFIX .. "injOnWriteStream just wrote, travelledDist: " .. (wheel.uytTravelledDist or -1) .. "m!")

				for _, visualWheel in ipairs(wheel.visualWheels) do
					for _, visualPart in ipairs(visualWheel.visualParts) do
						if visualPart:isa(WheelVisualPartTire) then
							streamWriteFloat32(streamId, visualPart.uytMaxUsage or -1)
							streamWriteFloat32(streamId, visualPart.uytWidth or -1)
							streamWriteFloat32(streamId, visualPart.uytMaxRadius or -1)
							streamWriteFloat32(streamId, visualPart.uytMaxPhysRadius or -1)
						end
					end
				end
			end
		end
	end
end
Wheels.onWriteStream = Utils.appendedFunction(Wheels.onWriteStream, UseYourTyres.injOnWriteStream)


function UseYourTyres.injOnReadUpdateStream(vehicle, streamId, _, connection)
	if connection.isServer and vehicle.uytHasTyres == true then
		local spec_wheels = vehicle.spec_wheels
		for _,  wheel in ipairs(spec_wheels.wheels) do
			local v1 = streamReadFloat32(streamId)
			if v1 ~= -1 then
				wheel.uytTravelledDist = v1
			end
			-- print(PRINT_PREFIX .. "injOnReadUpdateStream just read, travelledDist: " .. v1 .. "m!")
		end
	end
end
Wheels.onReadUpdateStream = Utils.appendedFunction(Wheels.onReadUpdateStream, UseYourTyres.injOnReadUpdateStream)


function UseYourTyres.injOnWriteUpdateStream(vehicle, streamId, connection)
	if not connection.isServer and vehicle.uytHasTyres == true then
		local spec_wheels = vehicle.spec_wheels
		for _,  wheel in ipairs(spec_wheels.wheels) do
			streamWriteFloat32(streamId, wheel.uytTravelledDist or -1)
			-- print(PRINT_PREFIX .. "injOnWriteUpdateStream just wrote, travelledDist: " .. (wheel.uytTravelledDist or -1) .. "m!")
		end
	end
end
Wheels.onWriteUpdateStream = Utils.appendedFunction(Wheels.onWriteUpdateStream, UseYourTyres.injOnWriteUpdateStream)


function UseYourTyres.getWearAmount(wheel)
	if wheel.uytTravelledDist == nil then
		return 0
	end

	local perimeter = (6.28 * wheel.physics.radius)
	local maxRevolutions = math.max(UseYourTyres.USED_MAX_M * UseYourTyres.tyreDistanceMultiplier / perimeter, 1)

	return math.clamp((wheel.uytTravelledDist / perimeter) / maxRevolutions, 0, 1)
end


function UseYourTyres.updateWheelRadius(wheelPartTire, isClientOnly, vehicle)
	if wheelPartTire.uytMaxUsage == nil or wheelPartTire.uytMaxRadius == nil or wheelPartTire.uytMaxPhysRadius == nil then
		return
	end

	local wheel = wheelPartTire.visualWheel.wheel
	local wheelWear = UseYourTyres.getWearAmount(wheel)
	local tireRadiusDiff = wheelWear * wheelPartTire.uytMaxUsage
	local usedTireRadius = wheelPartTire.uytMaxRadius - tireRadiusDiff

	-- ignore updates if change is not big enough, otherwise we risk stutters in MP sessions
	if wheelPartTire.uytCurrentRadius ~= nil and math.abs(wheelPartTire.uytCurrentRadius - usedTireRadius) < 0.001 then
		return
	end

	wheelPartTire.uytCurrentRadius = usedTireRadius

	if not isClientOnly then
		local usedTirePhysRadius = wheelPartTire.uytMaxPhysRadius - tireRadiusDiff
		wheel.physics.radius = usedTirePhysRadius
		wheel.physics:updateBase()
	end

	if wheelPartTire.uytSupportsVisualWear == true then
		for _, tire in ipairs(wheelPartTire.tireNodes) do
			setShaderParameter(tire, "usedTire", wheelPartTire.uytMaxRadius, wheelPartTire.uytWidth, wheelPartTire.uytCurrentRadius, wheelPartTire.uytMaxUsage, nil, false)
		end
	end

	if vehicle.uytWearReported == nil and wheelWear > 0.85 then
		-- Report wear
		local wearPercentage = string.format("%d%%", wheelWear * 100)
		local message = string.format(g_i18n:getText("ui_uytHighTyreWearWarning"), vehicle:getUppercaseName(), wearPercentage)
		g_currentMission:addIngameNotification(FSBaseMission.INGAME_NOTIFICATION_CRITICAL, message)
		
		-- Mark already reported
		vehicle.uytWearReported = true
	end
end

--[[ local RAYCAST_LENGTH = 2.0

function WheelVisualPartTire:useYourTireRaycastCallback(hitObjectId, x, y, z, distance, nx, ny, nz, subShapeIndex, shapeId, isLast)
	if distance < RAYCAST_LENGTH then        
        -- As it turns out raycasting don't seem to report correct distances, not sure why,
        -- thus I had to arbitrary get deduction that shall work across all tires.
        -- local rayDistanceCorrectionFactor = 0.5 * self.uytMaxRadius

        -- local correctedDistance = math.max((distance - RAYCAST_SAFE_OFFSET), 0)

        local correctedDistance = math.max(distance - RAYCAST_LENGTH, 0)
        self.uytMaxUsage = math.max(math.max(self.uytMaxUsage, correctedDistance), 0.001)

        local maxRadius = MathUtil.vector3Length(x - self.uytRaycastCenterX, y - self.uytRaycastCenterY, z - self.uytRaycastCenterZ)
        self.uytMaxRadius = math.max(self.uytMaxRadius, maxRadius)

        self.uytRaycastHits = self.uytRaycastHits + 1
        return true
	end
	return true
end ]]

function UseYourTyres.injWheelUpdate(wheel)
	local updateFriction = false

	for _, visualWheel in ipairs(wheel.visualWheels) do
		for _, visualPart in ipairs(visualWheel.visualParts) do
			if visualPart:isa(WheelVisualPartTire) and visualPart.node ~= nil and visualPart.uytEnabled ~= nil and visualPart.uytEnabled == true then
				-- always trigger friction recalculation if server and vehicle is moving
				updateFriction = updateFriction or (wheel.vehicle.isServer and wheel.vehicle:getLastSpeed() > 0.2)

				-- radius update and init should be done once per 4 sec
				local timeNow = getTimeSec()
				if visualPart.uytServerLastUpdate == nil or timeNow - visualPart.uytServerLastUpdate > 4 then
					if wheel.vehicle.isServer then
						for _, tire in ipairs(visualPart.tireNodes) do

							local prevMaterialId = getMaterial(tire, 0)
							local prevVariation = getMaterialCustomShaderVariation(prevMaterialId)

							if prevMaterialId ~= nil and prevVariation == "tirePressureDeformation" then

								-- Search for inner radius if not yet calculated!

								if visualPart.uytMaxUsage == nil then

									local _, y, _ = getScale(tire)
									local tireClone = clone(tire, false, false, false)
									setScale(tireClone, 0, 1, 1)
									link(getRootNode(), tireClone)
									local _, _, _, radius = getShapeWorldBoundingSphere(tireClone)
									unlink(tireClone)
									delete(tireClone)

									-- print(PRINT_PREFIX .. "Calculated radius: " ..  radius .. " vs. phys radius: " .. wheel.physics.radius)

									visualPart.uytMaxRadius = (radius + wheel.physics.radiusOriginal) * 0.5
									visualPart.uytWidth = wheel.physics.width
									visualPart.uytMaxPhysRadius = wheel.physics.radiusOriginal
									visualPart.uytMaxUsage = visualPart.maxDeformation * 0.75
									
									if wheel.uytReplacePrice == nil then
										wheel.uytReplacePrice = 0
									end
									wheel.uytReplacePrice = wheel.uytReplacePrice + (visualPart.uytMaxRadius * visualPart.uytWidth) * UseYourTyres.REPLACE_PRICE

									-- print(PRINT_PREFIX .. "Wheel set, radius: " .. visualPart.uytMaxRadius .. "; width: " .. visualPart.uytWidth .. "; physRadius: " .. visualPart.uytMaxPhysRadius .. "; maxUsage: " .. visualPart.uytMaxUsage)

									-- Disable feature for borked or fake wheels

									local radiusRatio = (wheel.physics.radiusOriginal / radius)
									if radiusRatio < 0.85 then
										visualPart.uytMaxRadius = nil
										visualPart.uytWidth = nil
										visualPart.uytMaxPhysRadius = nil
										visualPart.uytMaxUsage = nil
										visualPart.uytEnabled = false
										
										wheel.vehicle.uytHasTyres = false
										wheel.vehicle.uytUnsupported = true
									end
								end
							end
						end
					end
					UseYourTyres.updateWheelRadius(visualPart, not wheel.vehicle.isServer, wheel.vehicle)
					visualPart.uytServerLastUpdate = timeNow
				end
			end
		end
	end

	wheel.physics.isFrictionDirty = wheel.physics.isFrictionDirty or updateFriction
end
Wheel.update = Utils.appendedFunction(Wheel.update, UseYourTyres.injWheelUpdate)


function UseYourTyres.injWheelClientUpdate(wheel)
	local timeNow = getTimeSec()
	for _, visualWheel in ipairs(wheel.visualWheels) do
		for _, visualPart in ipairs(visualWheel.visualParts) do
			if visualPart.uytClientLastUpdate == nil or timeNow - visualPart.uytClientLastUpdate > 4 then
				UseYourTyres.updateWheelRadius(visualPart, true, wheel.vehicle)
				visualPart.uytClientLastUpdate = timeNow
				-- print(PRINT_PREFIX .. "Runnning client update, wheel travelled: " .. wheel.uytTravelledDist or -1 .. "m!")
			end
		end
	end
end
Wheel.clientUpdate = Utils.appendedFunction(Wheel.clientUpdate, UseYourTyres.injWheelClientUpdate)


function UseYourTyres.injPhysWheelUpdateContact(physWheel)
	if physWheel.vehicle.uytHasTyres ~= true then
		return
	end

	if physWheel.uytLastSpeedUpdate == nil then
		physWheel.uytLastSpeedUpdate = getTimeSec()
		return
	end

	local timeNow = getTimeSec()
	local dt = timeNow - physWheel.uytLastSpeedUpdate

	if dt < 0.001 then
		return
	end

	physWheel.uytLastSpeedUpdate = timeNow

	if physWheel.wheel.uytTravelledDist == nil then
		physWheel.wheel.uytTravelledDist = 0
	end

	if physWheel.contact == WheelContactType.GROUND or physWheel.contact == WheelContactType.OBJECT then
		local mps = physWheel.vehicle.lastSpeed * 1000
		physWheel.wheel.uytTravelledDist = physWheel.wheel.uytTravelledDist + mps * dt
	end

	-- print(PRINT_PREFIX .. "Update contact, new travvelled distance: " .. physWheel.wheel.uytTravelledDist or -1 .. "m!")
end
WheelPhysics.updateContact = Utils.appendedFunction(WheelPhysics.updateContact, UseYourTyres.injPhysWheelUpdateContact)

function UseYourTyres.injPhysWheelupdateTireFriction(physWheel)
	if physWheel.vehicle.uytHasTyres ~= true then
		return
	end

	if physWheel.vehicle.isServer and physWheel.vehicle.isAddedToPhysics then
		local wearAmountPowered = math.pow(UseYourTyres.getWearAmount(physWheel.wheel), 3) * 0.75
		local groundType = WheelsUtil.getGroundType(physWheel.densityType ~= FieldGroundType.NONE, physWheel.contact ~= WheelContactType.GROUND, physWheel.groundDepth)
		
		local wearFrictionScale
		if groundType == WheelsUtil.GROUND_ROAD then
			wearFrictionScale = 1 + wearAmountPowered
		else
			wearFrictionScale = math.clamp(1 - wearAmountPowered, 0.1, 1)
		end

		setWheelShapeTireFriction(physWheel.wheel.node, physWheel.wheelShape, physWheel.maxLongStiffness, physWheel.maxLatStiffness, physWheel.maxLatStiffnessLoad, wearFrictionScale * physWheel.frictionScale * physWheel.tireGroundFrictionCoeff)
		physWheel.isFrictionDirty = false
		-- print(PRINT_PREFIX .. "Updated friction!")
	end
end
WheelPhysics.updateTireFriction = Utils.appendedFunction(WheelPhysics.updateTireFriction, UseYourTyres.injPhysWheelupdateTireFriction)


function UseYourTyres.injVehicleShowInfo(vehicle, infoBox)
	if vehicle.uytHasTyres ~= true then
		if vehicle.uytHasTyres == false and vehicle.uytUnsupported == true then
			infoBox:addLine(g_i18n:getText("infohud_uytTyresWear"), g_i18n:getText("infohud_uytTyresWearUnsupported"))
		end
		return
	end

	if vehicle.spec_wheels ~= nil then
		local maxWear = -1
		for _, wheel in ipairs(vehicle.spec_wheels.wheels) do
			if wheel.uytTravelledDist ~= nil and wheel.uytTravelledDist > 0 then
				maxWear = math.max(maxWear, UseYourTyres.getWearAmount(wheel))
			end
		end

		if maxWear > -1 then
			infoBox:addLine(g_i18n:getText("infohud_uytTyresWear"), string.format("%d %%", maxWear * 100))
		end
	end
end
Vehicle.showInfo = Utils.appendedFunction(Vehicle.showInfo, UseYourTyres.injVehicleShowInfo)


function UseYourTyres.getTyresPrice(vehicle, isConfigChange)
	if vehicle == nil or vehicle.spec_wheels == nil then
		return 0
	end

	local cumulativePrice = 0
	for _, wheel in ipairs(vehicle.spec_wheels.wheels) do
		if wheel.uytReplacePrice ~= nil then
			if isConfigChange ~= nil then
				cumulativePrice = cumulativePrice + (wheel.uytReplacePrice * UseYourTyres.getWearAmount(wheel))
			else
				cumulativePrice = cumulativePrice + wheel.uytReplacePrice
			end
		end
	end

	return cumulativePrice
end


function UseYourTyres.onYesNoReplaceDialog(screen, isYes)
	if isYes then
		local tyresPrice =  UseYourTyres.getTyresPrice(screen.vehicle)
		if g_currentMission:getMoney() < tyresPrice then
			InfoDialog.show(g_i18n:getText("shop_messageNotEnoughMoneyToBuy"))
			return
		end
		
		g_client:getServerConnection():sendEvent(UytReplaceEvent.new(screen.vehicle, tyresPrice))
	end
end


function UseYourTyres.onReplaceTyresCallback(screen)
	local dialogString = string.format(g_i18n:getText("ui_uytReplaceDialog"), g_i18n:formatMoney(UseYourTyres.getTyresPrice(screen.vehicle), 0, true, true))
	local dialogCallback = UseYourTyres.onYesNoReplaceDialog
	local dialogSound = GuiSoundPlayer.SOUND_SAMPLES.CONFIG_WRENCH
	YesNoDialog.show(dialogCallback, screen, dialogString, nil, nil, nil, nil, dialogSound)
	return true
end


function UseYourTyres.injWokshopScreenOnOpen(screen)
	if screen.uytWorkshopInited == nil then
		-- Button
		local uytBtn = ButtonElement.new(screen.buttonsBox)
		uytBtn.name = "uytReplace"
		screen.buttonsBox:addElement(uytBtn)
		uytBtn:applyProfile("buttonActivate")
		uytBtn:setInputAction("UYT_REPLACE_TYRES")
		uytBtn.onClickCallback = function()
			UseYourTyres.onReplaceTyresCallback(screen)
		end
		uytBtn:setText(string.format("%s (%s)", g_i18n:getText("input_UYT_REPLACE_TYRES"), g_i18n:formatMoney(UseYourTyres.getTyresPrice(screen.vehicle), 0, true, true)))
		screen.uytBtn = uytBtn
		
		-- Separator
		local uytSep = BitmapElement.new(uytBtn)
		uytSep.name = "separator"
		uytBtn:addElement(uytSep)
		uytSep:applyProfile("fs25_buttonBoxSeparator")

		screen.uytWorkshopInited = true
	end

	--screen:toggleCustomInputContext(true, "MENU_UYT")
	local _, eventId = g_inputBinding:registerActionEvent("UYT_REPLACE_TYRES", screen, UseYourTyres.onReplaceTyresCallback, false, true, false, true)
	screen.uytEventId = eventId
end
if UseYourTyres.hasGui then
	WorkshopScreen.onOpen = Utils.appendedFunction(WorkshopScreen.onOpen, UseYourTyres.injWokshopScreenOnOpen)
end


function UseYourTyres.injWokshopScreenOnClose(screen)
	if screen.uytEventId ~= nil then
		g_inputBinding:removeActionEvent(screen.uytEventId)
		screen.uytEventId = nil
	end
	-- g_inputBinding:removeActionEventsByTarget(screen)
	-- screen:toggleCustomInputContext(false)
end
if UseYourTyres.hasGui then
	WorkshopScreen.onClose = Utils.appendedFunction(WorkshopScreen.onClose, UseYourTyres.injWokshopScreenOnClose)
end


function UseYourTyres.injWokshopScreenSetVehicle(screen, vehicle)
	if screen.uytBtn == nil then
		return
	end

	screen.uytBtn:setVisible(vehicle ~= nil and vehicle.uytHasTyres == true)
	
	if vehicle == nil then
		screen.uytBtn:setText(g_i18n:getText("input_UYT_REPLACE_TYRES"))
		screen.uytBtn:setDisabled(true)
	else
		screen.uytBtn:setText(string.format("%s (%s)", g_i18n:getText("input_UYT_REPLACE_TYRES"), g_i18n:formatMoney(UseYourTyres.getTyresPrice(vehicle), 0, true, true)))
		screen.uytBtn:setDisabled(false)
	end
end
if UseYourTyres.hasGui then
	WorkshopScreen.setVehicle = Utils.appendedFunction(WorkshopScreen.setVehicle, UseYourTyres.injWokshopScreenSetVehicle)
end


function UseYourTyres.injWheelsOnLoadFinished(vehicle, savegame)
	if vehicle.spec_wheels == nil then
		return
	end

	local oldTravelledDistances = nil
	if savegame ~= nil then
		-- Global tyre distance multiplier is saved as an attribute of the root vehicles node.
		local tyreDistanceMultiplier = savegame.xmlFile:getValue("vehicles#uytDistanceMultiplier")
		if tyreDistanceMultiplier ~= nil and tyreDistanceMultiplier ~= UseYourTyres.tyreDistanceMultiplier then
			UseYourTyres.tyreDistanceMultiplier = tyreDistanceMultiplier
		end

		-- try to recover version 1.0 travelled distances from savegame
		if vehicle.isServer and savegame.xmlFile ~= nil and savegame.xmlFile.filename ~= "" then
			oldTravelledDistances = {}
			for wheelIdx, wheel in ipairs(vehicle.spec_wheels.wheels) do
				local wheelKey = string.format("%s.wheels.wheel(%d)#uytTravelledDist", savegame.key, wheelIdx - 1)
				local travelDist = savegame.xmlFile:getValue(wheelKey)
				if travelDist ~= nil then
					oldTravelledDistances[wheelIdx] = travelDist
				end
			end
		end
		-- print(PRINT_PREFIX .. "Restoring wheel sstravels from configuration data, old travelled distances count: " .. (oldTravelledDistances ~= nil and #oldTravelledDistances or 0))
end

	UseYourTyres.restoreWheelTravelsFromConfigurationData(vehicle, oldTravelledDistances)
	UseYourTyres.saveWheelTravelsToConfigurationData(vehicle)
	-- print(PRINT_PREFIX .. "Uhm onLoadFinished, savegame: !" .. (savegame or "<nil>"))
end
Wheels.onLoadFinished = Utils.appendedFunction(Wheels.onLoadFinished, UseYourTyres.injWheelsOnLoadFinished)


Player.writeStream = Utils.appendedFunction(Player.writeStream, function(player, streamId, connection)
	streamWriteFloat32(streamId, UseYourTyres.tyreDistanceMultiplier)
end)


Player.readStream = Utils.appendedFunction(Player.readStream, function(player, streamId, connection)
	UseYourTyres.tyreDistanceMultiplier = streamReadFloat32(streamId)
end)


function UseYourTyres.getWheelConfigurationData(vehicle)
    if vehicle == nil or vehicle.configurations == nil then
        return nil
    end
    local configName = "wheel"
    local configId = vehicle.configurations[configName]
    if configId == nil then
        return nil
    end
    vehicle.configurationData = vehicle.configurationData or {}
    vehicle.configurationData[configName] = vehicle.configurationData[configName] or {}
    vehicle.configurationData[configName][configId] = vehicle.configurationData[configName][configId] or {}
    return vehicle.configurationData[configName][configId]
end

function UseYourTyres.saveWheelTravelsToConfigurationData(vehicle)
    if vehicle == nil or vehicle.spec_wheels == nil or vehicle.configurations == nil then
        return
    end
    local configName = "wheel"
    vehicle.configurationData = vehicle.configurationData or {}
    vehicle.configurationData[configName] = vehicle.configurationData[configName] or {}

    if vehicle.boughtConfigurations ~= nil and vehicle.boughtConfigurations[configName] ~= nil then
        for configId, _ in pairs(vehicle.boughtConfigurations[configName]) do
            vehicle.configurationData[configName][configId] = vehicle.configurationData[configName][configId] or { wheelTravels = {} }
        end
    end

    local data = UseYourTyres.getWheelConfigurationData(vehicle)
    if data == nil then
        return
    end
    data.wheelTravels = {}
    for wheelIdx, wheel in ipairs(vehicle.spec_wheels.wheels) do
        if wheel ~= nil then
            data.wheelTravels[wheelIdx] = wheel.uytTravelledDist or 0
        end
    end
end

function UseYourTyres.restoreWheelTravelsFromConfigurationData(vehicle, oldTravelledDistances)
    if vehicle == nil or vehicle.spec_wheels == nil or vehicle.configurations == nil then
        return
    end
    local configName = "wheel"
    local configId = vehicle.configurations[configName]
    if configId == nil then
        return
    end
    vehicle.configurationData = vehicle.configurationData or {}
    vehicle.configurationData[configName] = vehicle.configurationData[configName] or {}
    local configData = vehicle.configurationData[configName]
    if configData[configId] == nil then
        configData[configId] = { wheelTravels = {} }
    end
    local configEntry = configData[configId]
    local wheelTravels = configEntry.wheelTravels
    if wheelTravels == nil or #wheelTravels == 0 then
        configEntry.wheelTravels = {}
        for wheelIdx, wheel in ipairs(vehicle.spec_wheels.wheels) do
            if wheel ~= nil then
				if oldTravelledDistances ~= nil and oldTravelledDistances[wheelIdx] ~= nil then
					wheel.uytTravelledDist = oldTravelledDistances[wheelIdx]
					configEntry.wheelTravels[wheelIdx] = 0
				else	
					wheel.uytTravelledDist = 0
					configEntry.wheelTravels[wheelIdx] = 0
				end
            end
        end
        return
    end
    for wheelIdx, travelled in ipairs(wheelTravels) do
        local wheel = vehicle.spec_wheels.wheels[wheelIdx]
        if wheel ~= nil then
            wheel.uytTravelledDist = travelled or 0
        end
    end

	vehicle.uytWearReported = nil
end

function UseYourTyres.injVehicleConfigurationItemWheelSaveToXMLFile(configItem, xmlFile, configKey, isActive, configData)
    if configData == nil or configData.wheelTravels == nil then
        return
    end
    for wheelIdx, travelled in ipairs(configData.wheelTravels) do
        xmlFile:setValue(string.format("%s.wheels.wheel(%d)#uytTravelledDist", configKey, wheelIdx - 1), travelled or 0)
    end
end
VehicleConfigurationItemWheel.saveToXMLFile = Utils.appendedFunction(VehicleConfigurationItemWheel.saveToXMLFile, UseYourTyres.injVehicleConfigurationItemWheelSaveToXMLFile)

function UseYourTyres.injVehicleConfigurationItemWheelLoadFromSavegameXMLFile(configItem, xmlFile, configKey, configData)
    configData.wheelTravels = {}
    local wheelIdx = 0
    xmlFile:iterate(configKey .. ".wheels.wheel", function(_, wheelKey)
        wheelIdx = wheelIdx + 1
        local travelDist = xmlFile:getValue(wheelKey .. "#uytTravelledDist")
        configData.wheelTravels[wheelIdx] = travelDist or 0
    end)
end
VehicleConfigurationItemWheel.loadFromSavegameXMLFile = Utils.appendedFunction(VehicleConfigurationItemWheel.loadFromSavegameXMLFile, UseYourTyres.injVehicleConfigurationItemWheelLoadFromSavegameXMLFile)

function UseYourTyres.injVehicleConfigurationItemWheelRegisterSavegameXMLPaths(xmlSchema, baseKey)
    xmlSchema:register(XMLValueType.FLOAT, baseKey .. ".wheels.wheel(?)#uytTravelledDist", "Wheel travelled distance")
end
VehicleConfigurationItemWheel.registerSavegameXMLPaths = Utils.appendedFunction(VehicleConfigurationItemWheel.registerSavegameXMLPaths, UseYourTyres.injVehicleConfigurationItemWheelRegisterSavegameXMLPaths)

function UseYourTyres.injVehicleConfigurationItemWheelWriteToStream(configItem, streamId, connection, configData)
    local wheelTravels = configData ~= nil and configData.wheelTravels or nil
    if wheelTravels == nil or #wheelTravels == 0 then
        streamWriteInt32(streamId, 0)
        return
    end
    streamWriteInt32(streamId, #wheelTravels)
    for wheelIdx = 1, #wheelTravels do
        streamWriteFloat32(streamId, wheelTravels[wheelIdx] or 0)
    end
end
VehicleConfigurationItemWheel.writeToStream = UseYourTyres.injVehicleConfigurationItemWheelWriteToStream

function UseYourTyres.injVehicleConfigurationItemWheelReadFromStream(configItem, streamId, connection, configData)
    if configData == nil then
        return
    end
    local wheelCount = streamReadInt32(streamId)
    configData.wheelTravels = {}
    for wheelIdx = 1, wheelCount do
        configData.wheelTravels[wheelIdx] = streamReadFloat32(streamId)
    end
end
VehicleConfigurationItemWheel.readFromStream = UseYourTyres.injVehicleConfigurationItemWheelReadFromStream

function UseYourTyres.injVehicleSaveToXMLFile(vehicle, xmlFile, key, usedModNames)
    UseYourTyres.saveWheelTravelsToConfigurationData(vehicle)
    xmlFile:setValue("vehicles#uytDistanceMultiplier", UseYourTyres.tyreDistanceMultiplier)
end
Vehicle.saveToXMLFile = Utils.prependedFunction(Vehicle.saveToXMLFile, UseYourTyres.injVehicleSaveToXMLFile)

function UseYourTyres.preVehicleWriteStream(vehicle, streamId, connection)
    if vehicle ~= nil and vehicle.spec_wheels ~= nil then
        UseYourTyres.saveWheelTravelsToConfigurationData(vehicle)
    end
end
Vehicle.writeStream = Utils.prependedFunction(Vehicle.writeStream, UseYourTyres.preVehicleWriteStream)

function UseYourTyres.refreshVehicleWheelConfigurationData(vehicle)
    if vehicle == nil or vehicle.configurations == nil or vehicle.spec_wheels == nil then
        return
    end
    UseYourTyres.saveWheelTravelsToConfigurationData(vehicle)
end

function UseYourTyres.updateShopConfigurationData(configurationObject, vehicle)
    if configurationObject == nil or vehicle == nil then
        return
    end
    UseYourTyres.refreshVehicleWheelConfigurationData(vehicle)
    configurationObject.configurationData = table.clone(vehicle.configurationData, math.huge)
end

function UseYourTyres.onWorkshopSetConfigurations(workshop, configData, objectId)
    local vehicle = NetworkUtil.getObject(objectId)
    if vehicle ~= nil then
        UseYourTyres.updateShopConfigurationData(configData, vehicle)
    end
end
if UseYourTyres.hasGui then
	WorkshopScreen.setConfigurations = Utils.appendedFunction(WorkshopScreen.setConfigurations, UseYourTyres.onWorkshopSetConfigurations)
end

function UseYourTyres.onShopConfigYesNoBuy(shopConfigScreen, isYes)
    if not isYes or type(shopConfigScreen) ~= "table" or shopConfigScreen.vehicle == nil then
        return
    end
    UseYourTyres.refreshVehicleWheelConfigurationData(shopConfigScreen.vehicle)
    shopConfigScreen.configurationData = table.clone(shopConfigScreen.vehicle.configurationData, math.huge)
end
if UseYourTyres.hasGui and ShopConfigScreen ~= nil then
	ShopConfigScreen.onYesNoBuy = Utils.prependedFunction(ShopConfigScreen.onYesNoBuy, UseYourTyres.onShopConfigYesNoBuy)
end

function UseYourTyres.mergeConfigurationData(oldData, newData)
    if oldData == nil or newData == nil then
        return newData
    end
    for configName, configEntries in pairs(oldData) do
        if newData[configName] == nil then
            newData[configName] = table.clone(configEntries, math.huge)
        else
            for configId, configEntry in pairs(configEntries) do
                if newData[configName][configId] == nil then
                    newData[configName][configId] = table.clone(configEntry, math.huge)
                else
                    for key, value in pairs(configEntry) do
                        if newData[configName][configId][key] == nil then
                            newData[configName][configId][key] = value
                        end
                    end
                end
            end
        end
    end
    return newData
end

function UseYourTyres.preVehicleSetConfigurations(vehicle, configurations, boughtConfigurations, configurationData)
    if vehicle ~= nil then
        vehicle.uytPreviousConfigurationData = table.clone(vehicle.configurationData or {}, math.huge)
    end
    if vehicle ~= nil and vehicle.spec_wheels ~= nil and vehicle.configurations ~= nil then
        UseYourTyres.saveWheelTravelsToConfigurationData(vehicle)
    end
end
Vehicle.setConfigurations = Utils.prependedFunction(Vehicle.setConfigurations, UseYourTyres.preVehicleSetConfigurations)

function UseYourTyres.postVehicleSetConfigurations(vehicle, configurations, boughtConfigurations, configurationData)
    if vehicle ~= nil and vehicle.uytPreviousConfigurationData ~= nil and vehicle.configurationData ~= nil then
        vehicle.configurationData = UseYourTyres.mergeConfigurationData(vehicle.uytPreviousConfigurationData, vehicle.configurationData)
        vehicle.uytPreviousConfigurationData = nil
    end
    UseYourTyres.restoreWheelTravelsFromConfigurationData(vehicle)
end
Vehicle.setConfigurations = Utils.appendedFunction(Vehicle.setConfigurations, UseYourTyres.postVehicleSetConfigurations)


function UseYourTyres.injVehicleInit()
    Vehicle.xmlSchemaSavegame:register(XMLValueType.FLOAT, "vehicles#uytDistanceMultiplier", "Tyre distance multiplier")
	Vehicle.xmlSchemaSavegame:register(XMLValueType.FLOAT, "vehicles.vehicle(?).wheels.wheel(?)#uytTravelledDist", "Wheel travelled distance (old)")
end
Vehicle.init = Utils.appendedFunction(Vehicle.init, UseYourTyres.injVehicleInit)

if UseYourTyres.hasGui and g_workshopScreen ~= nil then
	g_workshopScreen = WorkshopScreen.createFromExistingGui(g_workshopScreen, "WorkshopScreen")
end

addModEventListener(UseYourTyres);
