if not TowBarMod then TowBarMod = {} end
if not TowBarMod.Utils then TowBarMod.Utils = {} end

TowBarMod.Utils.tempVector1 = Vector3f.new()
TowBarMod.Utils.tempVector2 = Vector3f.new()

---------------------------------------------------------------------------
--- Util functions
---------------------------------------------------------------------------

--- Compute the attachment Y offset for a vehicle so the towbar sits just
--- above the wheels (i.e. a fixed distance off the ground) regardless of
--- how the vehicle model is configured.
local function computeAttachmentHeight(vehicle)
	local script = vehicle:getScript()
	if not script then return -0.5 end

	local wheelCount = script:getWheelCount()
	if wheelCount > 0 then
		return script:getWheel(0):getOffset():y() + 0.1
	end

	return -0.5
end

local function getVehicleHalfLength(script)
	if not script then return nil end

	local ok, shape = pcall(function()
		return script:getPhysicsChassisShape()
	end)
	if ok and shape then
		local zOk, z = pcall(function()
			return shape:z()
		end)
		z = zOk and tonumber(z) or nil
		if z and z > 0 then return z / 2 end
	end

	ok, shape = pcall(function()
		return script:getExtents()
	end)
	if ok and shape then
		local zOk, z = pcall(function()
			return shape:z()
		end)
		z = zOk and tonumber(z) or nil
		if z and z > 0 then return z / 2 end
	end

	return nil
end

local function getExteriorAttachmentZ(script, attachmentId, originalZ, extraDistance)
	local halfLength = getVehicleHalfLength(script)
	if halfLength == nil then return originalZ end

	local configuredPadding = TowBarMod.Config and tonumber(TowBarMod.Config.towAttachmentExteriorPadding)
	local padding = configuredPadding or 0.25
	local direction = originalZ >= 0 and 1 or -1
	if originalZ == 0 and attachmentId == "trailer" then
		direction = -1
	end
	local exteriorZ = direction * (halfLength + padding + (extraDistance or 0))

	if math.abs(originalZ) > math.abs(exteriorZ) then
		return originalZ
	end

	return exteriorZ
end

function TowBarMod.Utils.isTrailer(vehicle)
	return string.match(string.lower(vehicle:getScript():getName()), "trailer")
end

--- Return vehicles from sector that player can tow by tow bar.
function TowBarMod.Utils.getAviableVehicles(mainVehicle, hasTowBar)
	local vehicles = {}
	if not hasTowBar then return vehicles end

	local square = mainVehicle:getSquare()
	if square == nil then return vehicles end

	-- Match vanilla towing search radius.
	for y=square:getY() - 6, square:getY() + 6 do
		for x=square:getX() - 6, square:getX() + 6 do
			local square2 = getCell():getGridSquare(x, y, square:getZ())
			if square2 then
				for i=1, square2:getMovingObjects():size() do
					local obj = square2:getMovingObjects():get(i-1)
					if obj ~= nil
						and instanceof(obj, "BaseVehicle")
						and obj ~= mainVehicle
						and #(TowBarMod.Utils.getHookTypeVariants(mainVehicle, obj, hasTowBar)) ~= 0 then
						table.insert(vehicles, obj)
					end
				end
			end
		end
	end

	return vehicles
end

--- Return a table with towbar-only hook options for vehicles.
function TowBarMod.Utils.getHookTypeVariants(vehicleA, vehicleB, hasTowBar)
	local hookTypeVariants = {}
	if not hasTowBar then return hookTypeVariants end

	if vehicleA:getVehicleTowing() or vehicleA:getVehicleTowedBy()
			or vehicleB:getVehicleTowing() or vehicleB:getVehicleTowedBy() then
		return hookTypeVariants
	end

	-- Keep tow bars for vehicle-to-vehicle towing only.
	if TowBarMod.Utils.isTrailer(vehicleA) or TowBarMod.Utils.isTrailer(vehicleB) then
		return hookTypeVariants
	end

	if vehicleA:canAttachTrailer(vehicleB, "trailerfront", "trailer") then
		local hookType = {}
		hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar")
		hookType.func = TowBarMod.Hook.attachByTowBarAction
		hookType.towingVehicle = vehicleB
		hookType.towedVehicle = vehicleA
		hookType.textureName = "tow_bar_icon"
		table.insert(hookTypeVariants, hookType)
	elseif vehicleA:canAttachTrailer(vehicleB, "trailer", "trailerfront") then
		local hookType = {}
		hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar")
		hookType.func = TowBarMod.Hook.attachByTowBarAction
		hookType.towingVehicle = vehicleA
		hookType.towedVehicle = vehicleB
		hookType.textureName = "tow_bar_icon"
		table.insert(hookTypeVariants, hookType)
	end

	return hookTypeVariants
end

function TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
	local towingScript = towingVehicle:getScript()
	local towedScript = towedVehicle:getScript()
	if towingScript == nil or towedScript == nil then return end

	local towingAttachment = towingScript:getAttachmentById(attachmentA)
	local towedAttachment = towedScript:getAttachmentById(attachmentB)
	if towingAttachment == nil or towedAttachment == nil then return end

	towingAttachment:setUpdateConstraint(false)
	towingAttachment:setZOffset(0)

	towedAttachment:setUpdateConstraint(false)
	towedAttachment:setZOffset(0)

	-- Dynamic height: compute Y from wheel offset so the towbar never clips the floor.
	local towingHeight = computeAttachmentHeight(towingVehicle)
	local towedHeight  = computeAttachmentHeight(towedVehicle)

	-- Store and update the towing vehicle's attachment Y.
	local towingModData = towingVehicle:getModData()
	local towingOffset = towingAttachment:getOffset()
	if towingModData["towBarOriginalTowingAttachmentId"] ~= attachmentA
			or towingModData["towBarOriginalTowingOffsetX"] == nil
			or towingModData["towBarOriginalTowingOffsetY"] == nil
			or towingModData["towBarOriginalTowingOffsetZ"] == nil then
		towingModData["towBarOriginalTowingOffsetX"] = towingOffset:x()
		towingModData["towBarOriginalTowingOffsetY"] = towingOffset:y()
		towingModData["towBarOriginalTowingOffsetZ"] = towingOffset:z()
		towingModData["towBarOriginalTowingAttachmentId"] = attachmentA
	end

	local towedModData = towedVehicle:getModData()
	local spacingDistance = 1.0
	if TowBarMod.Config and tonumber(TowBarMod.Config.rigidTowbarDistance) ~= nil then
		spacingDistance = tonumber(TowBarMod.Config.rigidTowbarDistance)
	end

	local towingBaseX = tonumber(towingModData["towBarOriginalTowingOffsetX"]) or towingOffset:x()
	local towingBaseZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"]) or towingOffset:z()
	local towingExteriorZ = getExteriorAttachmentZ(towingScript, attachmentA, towingBaseZ, 0)
	towingAttachment:getOffset():set(towingBaseX, towingHeight, towingExteriorZ)

	local offset = towedAttachment:getOffset()
	local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"])
	local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"])
	local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"])
	local hasStoredBase = towedModData["towBarBaseAttachmentId"] == attachmentB
		and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil

	local baseX = hasStoredBase and storedBaseX or offset:x()
	local baseY = hasStoredBase and storedBaseY or offset:y()
	local baseZ = hasStoredBase and storedBaseZ or offset:z()

	if not hasStoredBase then
		towedModData["towBarBaseAttachmentId"] = attachmentB
		towedModData["towBarBaseAttachmentOffsetX"] = baseX
		towedModData["towBarBaseAttachmentOffsetY"] = baseY
		towedModData["towBarBaseAttachmentOffsetZ"] = baseZ
	end

	local towedExteriorZ = getExteriorAttachmentZ(towedScript, attachmentB, baseZ, spacingDistance)
	local zShift = towedExteriorZ - baseZ
	towedAttachment:getOffset():set(baseX, towedHeight, towedExteriorZ)

	towedModData["isChangedTowedAttachment"] = true
	towedModData["towBarChangedAttachmentId"] = attachmentB
	towedModData["towBarChangedOffsetZShift"] = zShift
	towedVehicle:transmitModData()
	towingVehicle:transmitModData()
end

function TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
	local towingModData = towingVehicle:getModData()
	local towingAttachmentId = towingModData["towBarOriginalTowingAttachmentId"]
		or towingVehicle:getTowAttachmentSelf()
	local towingAttachment = towingVehicle:getScript():getAttachmentById(towingAttachmentId)
	if towingAttachment ~= nil then
		towingAttachment:setUpdateConstraint(true)
		local zOffset = (towingAttachmentId == "trailer") and -1 or 1
		towingAttachment:setZOffset(zOffset)

		-- Restore the original offset that was overridden for rigid tow spacing.
		local originalX = tonumber(towingModData["towBarOriginalTowingOffsetX"])
		local originalY = tonumber(towingModData["towBarOriginalTowingOffsetY"])
		local originalZ = tonumber(towingModData["towBarOriginalTowingOffsetZ"])
		if originalX ~= nil and originalY ~= nil and originalZ ~= nil then
			towingAttachment:getOffset():set(originalX, originalY, originalZ)
		elseif originalY ~= nil then
			local off = towingAttachment:getOffset()
			towingAttachment:getOffset():set(off:x(), originalY, off:z())
		end
	end
	towingModData["towBarOriginalTowingOffsetX"] = nil
	towingModData["towBarOriginalTowingOffsetY"] = nil
	towingModData["towBarOriginalTowingOffsetZ"] = nil
	towingModData["towBarOriginalTowingAttachmentId"] = nil
	towingVehicle:transmitModData()

	local towedModData = towedVehicle:getModData()
	local changedAttachmentId = towedModData["towBarChangedAttachmentId"] or towedVehicle:getTowAttachmentSelf()
	local towedAttachment = towedVehicle:getScript():getAttachmentById(changedAttachmentId)
	if towedAttachment ~= nil then
		towedAttachment:setUpdateConstraint(true)
		local zOffset = (changedAttachmentId == "trailer") and -1 or 1
		towedAttachment:setZOffset(zOffset)

		if towedModData["isChangedTowedAttachment"] then
			local storedBaseX = tonumber(towedModData["towBarBaseAttachmentOffsetX"])
			local storedBaseY = tonumber(towedModData["towBarBaseAttachmentOffsetY"])
			local storedBaseZ = tonumber(towedModData["towBarBaseAttachmentOffsetZ"])
			local hasStoredBase = towedModData["towBarBaseAttachmentId"] == changedAttachmentId
				and storedBaseX ~= nil and storedBaseY ~= nil and storedBaseZ ~= nil

			if hasStoredBase then
				towedAttachment:getOffset():set(storedBaseX, storedBaseY, storedBaseZ)
			else
				local offset = towedAttachment:getOffset()
				local storedShift = tonumber(towedModData["towBarChangedOffsetZShift"])
				if storedShift ~= nil then
					towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() - storedShift)
				else
					local zShift = offset:z() > 0 and -1 or 1
					towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift)
				end
			end
		end
	end

	towedModData["isChangedTowedAttachment"] = false
	towedModData["towBarChangedAttachmentId"] = nil
	towedModData["towBarChangedOffsetZShift"] = nil
	towedModData["towBarBaseAttachmentId"] = nil
	towedModData["towBarBaseAttachmentOffsetX"] = nil
	towedModData["towBarBaseAttachmentOffsetY"] = nil
	towedModData["towBarBaseAttachmentOffsetZ"] = nil
	towedVehicle:transmitModData()
end

-----------------------------------------------------------

--- Fix mods that add vehicles without tow attachments
local function fixTowAttachmentsForOtherVehicleMods()
	local scriptManager = getScriptManager()
	local vehicleScripts = scriptManager:getAllVehicleScripts()

	for i = 0, vehicleScripts:size()-1 do
		local script = vehicleScripts:get(i)
		local wheelCount = script:getWheelCount()

		local attachHeigtOffset = -0.5
		if wheelCount > 0 then
			attachHeigtOffset = script:getWheel(0):getOffset():y() + 0.1
		end

		if not string.match(string.lower(script:getName()), "trailer") then
			local trailerAttachment = script:getAttachmentById("trailer")
			if trailerAttachment == nil then
				local attach = ModelAttachment.new("trailer")
				attach:getOffset():set(0, attachHeigtOffset, -script:getPhysicsChassisShape():z()/2 - 0.1)
				attach:setZOffset(-1)
				script:addAttachment(attach)
			end

			local trailerFrontAttachment = script:getAttachmentById("trailerfront")
			if trailerFrontAttachment == nil then
				local attach = ModelAttachment.new("trailerfront")
				attach:getOffset():set(0, attachHeigtOffset, script:getPhysicsChassisShape():z()/2 + 0.1)
				attach:setZOffset(1)
				script:addAttachment(attach)
			end
		end
	end
end

Events.OnGameBoot.Add(fixTowAttachmentsForOtherVehicleMods)
