GroundAdjustedNodesExtended = {
	MOD_NAME = g_currentModName,
	SPEC_NAME = g_currentModName .. ".groundAdjustedNodesExtended"
}
GroundAdjustedNodesExtended.SPEC_TABLE_NAME = "spec_" .. GroundAdjustedNodesExtended.SPEC_NAME
GroundAdjustedNodesExtended.GROUND_ADJUSTED_NODE_XML_KEY = "vehicle.groundAdjustedNodesExtended.groundAdjustedNode(?)"
GroundAdjustedNodesExtended.COLLISION_MASK = CollisionFlag.TERRAIN + CollisionFlag.STATIC_OBJECT

function GroundAdjustedNodesExtended.prerequisitesPresent(specializations)
	return true
end

function GroundAdjustedNodesExtended.initSpecialization()
	local schema = Vehicle.xmlSchema

	schema:setXMLSpecializationType("GroundAdjustedNodesExtended")

	local basePath = GroundAdjustedNodesExtended.GROUND_ADJUSTED_NODE_XML_KEY

	schema:register(XMLValueType.FLOAT, "vehicle.groundAdjustedNodesExtended#maxUpdateDistance", "If the player is more than this distance away the nodes will no longer be updated", 100)
	schema:register(XMLValueType.FLOAT, "vehicle.groundAdjustedNodesExtended#maxUpdateDistanceWobble", "If the player is more than this distance away the wobble effect which is applied on the field will not be shown anymore", 50)
	schema:register(XMLValueType.BOOL, "vehicle.groundAdjustedNodesExtended#onlyActiveWhileAttached", "Defines if the tool needs to be attached to have the ground adjusted nodes active", true)
	schema:register(XMLValueType.FLOAT, basePath .. "#activationTime", "In this time after the activation of the node the #moveSpeedStateChange will be used", 0)
	schema:register(XMLValueType.FLOAT, basePath .. "#yOffset", "Raycast Y translation offset (Raycast will start this distance above the node)")
	schema:register(XMLValueType.FLOAT, basePath .. ".foldable#minLimit", "Fold min. time", 0)
	schema:register(XMLValueType.FLOAT, basePath .. ".foldable#maxLimit", "Fold max. time", 1)
	schema:register(XMLValueType.NODE_INDEX, basePath .. ".raycastNode(?)#node", "Ground adjusted raycast node")
	schema:register(XMLValueType.FLOAT, basePath .. ".raycastNode(?)#distance", "Ground adjusted raycast distance", 4)
	schema:register(XMLValueType.INT, basePath .. ".raycastNode(?)#updateFrame", "Defines the frame delay between two raycasts", "Number of raycasts")
	schema:register(XMLValueType.FLOAT, basePath .. ".raycastNode(?)#yOffset", "Raycast Y translation offset (Raycast will start this distance above the node)", 0)
	GroundAdjustedNodesExtended.registerAdjustNodeXMLPaths(schema, basePath)
	GroundAdjustedNodesExtended.registerAdjustNodeXMLPaths(schema, basePath .. ".adjustNode(?)")
	schema:setXMLSpecializationType()
end

function GroundAdjustedNodesExtended.registerAdjustNodeXMLPaths(schema, basePath)
	schema:register(XMLValueType.NODE_INDEX, basePath .. "#node", "Ground adjusted node")
	schema:register(XMLValueType.FLOAT, basePath .. "#minY", "Min. Y translation", "translation in i3d - 1")
	schema:register(XMLValueType.FLOAT, basePath .. "#maxY", "Max. Y translation", "minY + 1")
	schema:register(XMLValueType.FLOAT, basePath .. "#moveSpeed", "Move speed", 1)
	schema:register(XMLValueType.BOOL, basePath .. "#resetIfNotActive", "Reset node to start translation if not active", true)
	schema:register(XMLValueType.FLOAT, basePath .. "#moveSpeedStateChange", "Move speed while node is inactive or active an in range of #activationTime", "#moveSpeed")
	schema:register(XMLValueType.FLOAT, basePath .. "#updateThreshold", "Position of node will be updated if change is greater than this value", 0.002)
	schema:register(XMLValueType.FLOAT, basePath .. "#inActiveOffsetY", "Offset of the in active position in Y, will be applied on top of the current position in i3d", 0)
	schema:register(XMLValueType.FLOAT, basePath .. "#inActiveY", "Adjust node will go to this state while it's not active", "Position in i3d file")
	schema:register(XMLValueType.BOOL, basePath .. "#averageInActivePosY", "While nodes are turned off the average Y position will be used as target for all nodes", false)
end

function GroundAdjustedNodesExtended.registerFunctions(vehicleType)
	SpecializationUtil.registerFunction(vehicleType, "loadGroundAdjustedExtNodeFromXML", GroundAdjustedNodesExtended.loadGroundAdjustedExtNodeFromXML)
	SpecializationUtil.registerFunction(vehicleType, "loadGroundAdjustedExtAdjustNodeFromXML", GroundAdjustedNodesExtended.loadGroundAdjustedExtAdjustNodeFromXML)
	SpecializationUtil.registerFunction(vehicleType, "initGroundAdjustedExtAdjustNode", GroundAdjustedNodesExtended.initGroundAdjustedExtAdjustNode)
	SpecializationUtil.registerFunction(vehicleType, "loadGroundAdjustedExtRaycastNodeFromXML", GroundAdjustedNodesExtended.loadGroundAdjustedExtRaycastNodeFromXML)
	SpecializationUtil.registerFunction(vehicleType, "getIsGroundAdjustedExtNodeActive", GroundAdjustedNodesExtended.getIsGroundAdjustedExtNodeActive)
	SpecializationUtil.registerFunction(vehicleType, "updateGroundAdjustedExtRaycasts", GroundAdjustedNodesExtended.updateGroundAdjustedExtRaycasts)
	SpecializationUtil.registerFunction(vehicleType, "updateGroundAdjustedExtNode", GroundAdjustedNodesExtended.updateGroundAdjustedExtNode)
end

function GroundAdjustedNodesExtended.registerEventListeners(vehicleType)
	SpecializationUtil.registerEventListener(vehicleType, "onLoad", GroundAdjustedNodesExtended)
	SpecializationUtil.registerEventListener(vehicleType, "onLoadFinished", GroundAdjustedNodesExtended)
	SpecializationUtil.registerEventListener(vehicleType, "onUpdate", GroundAdjustedNodesExtended)
end

function GroundAdjustedNodesExtended:onLoad(savegame)
	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]
	spec.raycastNodesByNode = {}
	spec.maxUpdateDistance = self.xmlFile:getValue("vehicle.groundAdjustedNodesExtended#maxUpdateDistance", 100)
	spec.maxUpdateDistanceWobble = self.xmlFile:getValue("vehicle.groundAdjustedNodesExtended#maxUpdateDistanceWobble", 50)
	spec.onlyActiveWhileAttached = self.xmlFile:getValue("vehicle.groundAdjustedNodesExtended#onlyActiveWhileAttached", true)
	spec.groundAdjustedNodes = {}

	self.xmlFile:iterate("vehicle.groundAdjustedNodesExtended.groundAdjustedNode", function (index, key)
		local groundAdjustedNode = {}

		if self:loadGroundAdjustedExtNodeFromXML(self.xmlFile, key, groundAdjustedNode) then
			table.insert(spec.groundAdjustedNodes, groundAdjustedNode)
		end
	end)

	if #spec.groundAdjustedNodes == 0 then
		SpecializationUtil.removeEventListener(self, "onUpdate", GroundAdjustedNodesExtended)
	end
end

function GroundAdjustedNodesExtended:onLoadFinished(savegame)
	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]

	for _, groundAdjustedNode in pairs(spec.groundAdjustedNodes) do
		for i = 1, #groundAdjustedNode.adjustNodes do
			local adjustNode = groundAdjustedNode.adjustNodes[i]

			setTranslation(adjustNode.node, adjustNode.x, adjustNode.curY, adjustNode.z)

			if self.setMovingToolDirty ~= nil then
				self:setMovingToolDirty(adjustNode.node)
			end
		end
	end
end

function GroundAdjustedNodesExtended:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]

	if self.currentUpdateDistance < spec.maxUpdateDistance then
		self:updateGroundAdjustedExtRaycasts(dt)

		for _, groundAdjustedNode in pairs(spec.groundAdjustedNodes) do
			self:updateGroundAdjustedExtNode(groundAdjustedNode, dt)
		end
	end
end

function GroundAdjustedNodesExtended:loadGroundAdjustedExtNodeFromXML(xmlFile, key, groundAdjustedNode)
	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]
	groundAdjustedNode.yOffset = xmlFile:getValue(key .. "#yOffset")
	groundAdjustedNode.activationTime = xmlFile:getValue(key .. "#activationTime", 0) * 1000
	groundAdjustedNode.activationTimer = 0
	groundAdjustedNode.foldMinLimit = xmlFile:getValue(key .. ".foldable#minLimit", 0)
	groundAdjustedNode.foldMaxLimit = xmlFile:getValue(key .. ".foldable#maxLimit", 1)
	groundAdjustedNode.adjustNodes = {}
	local baseAdjustNode = {}

	if self:loadGroundAdjustedExtAdjustNodeFromXML(xmlFile, key, groundAdjustedNode, baseAdjustNode, false) then
		table.insert(groundAdjustedNode.adjustNodes, baseAdjustNode)
	end

	xmlFile:iterate(key .. ".adjustNode", function (index, adjustKey)
		local adjustNode = {}

		if self:loadGroundAdjustedExtAdjustNodeFromXML(xmlFile, adjustKey, groundAdjustedNode, adjustNode, true) then
			table.insert(groundAdjustedNode.adjustNodes, adjustNode)
		end
	end)

	groundAdjustedNode.raycastNodes = {}

	xmlFile:iterate(key .. ".raycastNode", function (index, raycastKey)
		local raycastNode = self:loadGroundAdjustedExtRaycastNodeFromXML(xmlFile, raycastKey, groundAdjustedNode, {})

		if raycastNode ~= nil then
			if #groundAdjustedNode.raycastNodes > 2 then
				Logging.xmlWarning(self.xmlFile, "Max. two raycast nodes are allowed per groundAdjustedNode! (%s)", key)

				return
			end

			table.insert(groundAdjustedNode.raycastNodes, raycastNode)

			spec.raycastNodesByNode[raycastNode.node] = raycastNode
		end
	end)

	for i = 1, #groundAdjustedNode.adjustNodes do
		self:initGroundAdjustedExtAdjustNode(groundAdjustedNode, groundAdjustedNode.adjustNodes[i])
	end

	if #groundAdjustedNode.raycastNodes > 0 and #groundAdjustedNode.adjustNodes > 0 then
		local parent = nil

		for i = 1, #groundAdjustedNode.raycastNodes do
			local raycastNode = groundAdjustedNode.raycastNodes[i]

			if parent ~= nil and getParent(raycastNode.node) ~= parent then
				Logging.xmlWarning(self.xmlFile, "Not all raycasts and adjust nodes are on the same hierarchy level '%s'!", key)

				return false
			end

			parent = getParent(raycastNode.node)
		end

		groundAdjustedNode.isActive = false

		return true
	else
		Logging.xmlWarning(self.xmlFile, "No raycastNodes or adjust nodes defined for groundAdjustedNode '%s'!", key)

		return false
	end
end

function GroundAdjustedNodesExtended:loadGroundAdjustedExtRaycastNodeFromXML(xmlFile, key, groundAdjustedNode, raycastNode)
	XMLUtil.checkDeprecatedXMLElements(xmlFile, self.configFileName, key .. "#index", key .. "#node")

	local node = xmlFile:getValue(key .. "#node", nil, self.components, self.i3dMappings)

	if node == nil then
		Logging.xmlWarning(xmlFile, "Missing 'node' for groundAdjustedNodes raycast '%s'!", key)

		return false
	end

	raycastNode.node = node
	raycastNode.maxDistance = xmlFile:getValue(key .. "#distance", 4)
	raycastNode.lastRaycastDistance = raycastNode.maxDistance
	raycastNode.yOffset = xmlFile:getValue(key .. "#yOffset", groundAdjustedNode.yOffset or 0)
	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]

	if spec.raycastNodesByNode[node] ~= nil then
		local otherRaycastNode = spec.raycastNodesByNode[node]

		if math.abs(raycastNode.yOffset - otherRaycastNode.yOffset) < 0.01 and math.abs(raycastNode.maxDistance - otherRaycastNode.maxDistance) < 0.01 then
			return spec.raycastNodesByNode[node]
		else
			Logging.xmlWarning(xmlFile, "Found multiple groundAdjustedNode raycasts with different settings for '%s'!", getName(node))

			return nil
		end
	end

	function raycastNode.groundAdjustExtRaycastCallback(_, transformId, x, y, z, distance)
		if getHasTrigger(transformId) then
			return true
		end

		if transformId ~= 0 then
			raycastNode.lastRaycastDistance = distance
		end

		return false
	end

	raycastNode.parent = groundAdjustedNode

	return raycastNode
end

function GroundAdjustedNodesExtended:loadGroundAdjustedExtAdjustNodeFromXML(xmlFile, key, groundAdjustedNode, adjustNode, required)
	local node = xmlFile:getValue(key .. "#node", nil, self.components, self.i3dMappings)

	if node == nil then
		if required == true then
			Logging.xmlWarning(xmlFile, "Missing 'node' for groundAdjustedNode '%s'!", key)
		end

		return false
	end

	local x, y, z = getTranslation(node)
	adjustNode.node = node
	adjustNode.x = x
	adjustNode.y = y
	adjustNode.z = z
	adjustNode.minY = xmlFile:getValue(key .. "#minY", y - 1)
	adjustNode.maxY = xmlFile:getValue(key .. "#maxY", adjustNode.minY + 1)
	adjustNode.moveSpeed = xmlFile:getValue(key .. "#moveSpeed", 1) / 1000
	adjustNode.moveSpeedStateChange = xmlFile:getValue(key .. "#moveSpeedStateChange", adjustNode.moveSpeed * 1000) / 1000
	adjustNode.resetIfNotActive = xmlFile:getValue(key .. "#resetIfNotActive", true)
	adjustNode.updateThreshold = xmlFile:getValue(key .. "#updateThreshold", 0.002)
	adjustNode.inActiveY = xmlFile:getValue(key .. "#inActiveY", y) + xmlFile:getValue(key .. "#inActiveOffsetY", 0)
	adjustNode.averageInActivePosY = xmlFile:getValue(key .. "#averageInActivePosY", false)
	adjustNode.targetY = adjustNode.inActiveY
	adjustNode.curY = adjustNode.inActiveY
	adjustNode.lastY = adjustNode.inActiveY
	adjustNode.lastOffsetDistance = math.random() * 10000

	return true
end

function GroundAdjustedNodesExtended:initGroundAdjustedExtAdjustNode(groundAdjustedNode, adjustNode)
	if #groundAdjustedNode.raycastNodes == 2 then
		local x1, _, z1 = localToLocal(adjustNode.node, getParent(groundAdjustedNode.raycastNodes[1].node), 0, 0, 0)
		local x2, _, z2 = localToLocal(groundAdjustedNode.raycastNodes[1].node, getParent(groundAdjustedNode.raycastNodes[1].node), 0, 0, 0)
		local x3, _, z3 = localToLocal(groundAdjustedNode.raycastNodes[2].node, getParent(groundAdjustedNode.raycastNodes[1].node), 0, 0, 0)
		local dirX = x3 - x2
		local dirZ = z3 - z2
		local length = MathUtil.vector2Length(x3 - x2, z3 - z2)
		dirX, dirZ = MathUtil.vector2Normalize(dirX, dirZ)
		adjustNode.alpha = MathUtil.getProjectOnLineParameter(x1, z1, x2, z2, dirX, dirZ) / length
	end
end

function GroundAdjustedNodesExtended:updateGroundAdjustedExtRaycasts(dt)
	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]

	for node, raycastNode in pairs(spec.raycastNodesByNode) do
		if raycastNode.parent.isActive then
			local x, y, z = localToWorld(raycastNode.node, 0, raycastNode.yOffset, 0)
			raycastNode.lastIsOnField = getDensityAtWorldPos(g_currentMission.terrainDetailId, x, 0, z) ~= 0

			if not raycastNode.lastIsOnField then
				local dx, dy, dz = localDirectionToWorld(raycastNode.node, 0, -1, 0)

				raycastAll(x, y, z, dx, dy, dz, "groundAdjustExtRaycastCallback", raycastNode.maxDistance, raycastNode, GroundAdjustedNodesExtended.COLLISION_MASK, false, false)
			else
				local terrainHeight = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z)

				if y > terrainHeight - 10 then
					raycastNode.lastRaycastDistance = math.min(y - terrainHeight, raycastNode.maxDistance)
				end
			end
		end
	end
end

local PERLIN_NOISE = {
	randomFrequency = 3.5,
	numOctaves = 6,
	randomSeed = 99,
	persistence = 0,
	maxOffset = 0.015
}
local sin = math.sin

local function getRandomOffset(t)
	return sin(t * 15.7079) * 0.4 + sin(t * 41.46902) * 0.4 + sin(t * 62.83185) * 0.3
end

function GroundAdjustedNodesExtended:updateGroundAdjustedExtNode(groundAdjustedNode, dt)
	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]
	local wasActive = groundAdjustedNode.isActive
	groundAdjustedNode.isActive = self:getIsGroundAdjustedExtNodeActive(groundAdjustedNode)

	if groundAdjustedNode.isActive then
		groundAdjustedNode.activationTimer = math.max(groundAdjustedNode.activationTimer - dt, 0)
	else
		if groundAdjustedNode.averageInActivePosY and wasActive then
			local groundAdjustedNodes = spec.groundAdjustedNodes
			local inActiveY = 0
			local numNodes = 0

			for _, _groundAdjustedNode in pairs(groundAdjustedNodes) do
				for i = 1, #_groundAdjustedNode.adjustNodes do
					local _adjustNode = _groundAdjustedNode.adjustNodes[i]

					if _adjustNode.averageInActivePosY then
						inActiveY = inActiveY + _adjustNode.curY
						numNodes = numNodes + 1
					end
				end
			end

			if numNodes > 0 then
				groundAdjustedNode.inActiveY = inActiveY / numNodes

				for _, _groundAdjustedNode in pairs(groundAdjustedNodes) do
					for i = 1, #_groundAdjustedNode.adjustNodes do
						local _adjustNode = _groundAdjustedNode.adjustNodes[i]

						if _adjustNode.averageInActivePosY then
							_adjustNode.inActiveY = inActiveY / numNodes
						end
					end
				end
			end
		end

		groundAdjustedNode.activationTimer = groundAdjustedNode.activationTime
	end

	for i = 1, #groundAdjustedNode.adjustNodes do
		local adjustNode = groundAdjustedNode.adjustNodes[i]

		if groundAdjustedNode.isActive then
			if not wasActive then
				local _, y, _ = getTranslation(adjustNode.node)
				adjustNode.curY = y
			end

			local raycastNode = groundAdjustedNode.raycastNodes[1]
			local height = nil

			if #groundAdjustedNode.raycastNodes == 2 then
				height = groundAdjustedNode.raycastNodes[1].lastRaycastDistance * (1 - adjustNode.alpha) + groundAdjustedNode.raycastNodes[2].lastRaycastDistance * adjustNode.alpha
			else
				height = groundAdjustedNode.raycastNodes[1].lastRaycastDistance
			end

			local _, targetY, _ = localToLocal(raycastNode.node, getParent(adjustNode.node), 0, raycastNode.yOffset - height, 0)

			if self.currentUpdateDistance < spec.maxUpdateDistanceWobble and raycastNode.lastIsOnField then
				adjustNode.lastOffsetDistance = adjustNode.lastOffsetDistance + self.lastMovedDistance * self.movingDirection * 0.1
				local noise = getRandomOffset(adjustNode.lastOffsetDistance)
				targetY = targetY + noise * PERLIN_NOISE.maxOffset
			end

			adjustNode.targetY = MathUtil.clamp(targetY, adjustNode.minY, adjustNode.maxY)
		elseif adjustNode.resetIfNotActive then
			adjustNode.targetY = adjustNode.inActiveY
		end

		if adjustNode.targetY ~= adjustNode.curY then
			local stateChangeActive = not groundAdjustedNode.isActive or groundAdjustedNode.activationTimer > 0
			local moveSpeed = stateChangeActive and adjustNode.moveSpeedStateChange or adjustNode.moveSpeed

			if adjustNode.curY < adjustNode.targetY then
				adjustNode.curY = math.min(adjustNode.curY + moveSpeed * dt, adjustNode.targetY)
			else
				adjustNode.curY = math.max(adjustNode.curY - moveSpeed * dt, adjustNode.targetY)
			end

			if adjustNode.updateThreshold < math.abs(adjustNode.lastY - adjustNode.curY) then
				setTranslation(adjustNode.node, adjustNode.x, adjustNode.curY, adjustNode.z)

				adjustNode.lastY = adjustNode.curY

				if self.setMovingToolDirty ~= nil then
					self:setMovingToolDirty(adjustNode.node)
				end
			end
		end
	end
end

function GroundAdjustedNodesExtended:getIsGroundAdjustedExtNodeActive(groundAdjustedNode)
	local spec_foldable = self.spec_foldable

	if spec_foldable ~= nil then
		local foldAnimTime = spec_foldable.foldAnimTime

		if foldAnimTime ~= nil and (groundAdjustedNode.foldMaxLimit < foldAnimTime or foldAnimTime < groundAdjustedNode.foldMinLimit) then
			return false
		end
	end

	local spec = self[GroundAdjustedNodesExtended.SPEC_TABLE_NAME]

	return not spec.onlyActiveWhileAttached or self.getAttacherVehicle == nil or self:getAttacherVehicle() ~= nil
end
