--
-- RealTerrain - realTerrain.lua
--
-- Author:  		Satis and Disco @ OEB Modding
-- Date:    		10-05-15
-- Time:    		00:01
-- Last Updated:	09-08-15
--
-- Copyright (C) OEB Modding - www.oebmodding.co.uk
--
local groundTypes = {
	[WheelsUtil.GROUND_ROAD] = "Road",
	[WheelsUtil.GROUND_HARD_TERRAIN] = "Hard Terrain",
	[WheelsUtil.GROUND_SOFT_TERRAIN] = "Soft Terrain",
	[WheelsUtil.GROUND_FIELD] = "Field"
};

realTerrain = {
	printDebug = false,
	printWheelInfo = false
};

function realTerrain.prerequisitesPresent(specializations)
    return true;
end;

function realTerrain:load(xmlFile)
	self.hasPloughAttached = realTerrain.hasPloughAttached;

	-- We only need to run this code on the server.
	if self.isServer and self.wheels and #self.wheels > 0 then
		if printDebug and self.name == nil then
			if self.configFileName then
				local storeItem = StoreItemsUtil.storeItemsByXMLFilename[self.configFileName:lower()];
				if storeItem and storeItem.name then
					self.name = storeItem.name;
				end;
			end;

			if self.name == nil then
				local nameSearch = { 'vehicle.name.' .. g_languageShort, 'vehicle.name.en', 'vehicle.name', 'vehicle#type' };
				local name;
				for i,xmlKey in ipairs(nameSearch) do
					name = getXMLString(xmlFile, xmlKey);
					if name ~= nil then
						self.name = name;
						break;
					end;
				end;
			end;
		end;

		self.realTerrain = {
			networkUpdate = false,
			isDirty = false,
			hasPloughAttached = false,
			extraDrop = false,
			terrainType = {
				-- Field terrain
				[WheelsUtil.GROUND_FIELD] = {
					-- Cultivated Density
					[2^g_currentMission.cultivatorChannel] = {
						defDropDis = 0.08,			-- (In meters) Default drop distance
						leftRightDropDis = 0,    	-- Keep at 0. Only used my ploughChannel
						minBumpHeight = 0.005,      -- (In meters) Minimum Bump height to generate
						maxBumpHeight = 0.02        -- (In meters) Maximum Bump height to generate
					},
					-- Ploughed Density
					[2^g_currentMission.ploughChannel] = {
						defDropDis = 0.16,			-- (In meters) Default drop distance
						leftRightDropDis = 0.1,		-- (In meters) Added drop distance if only either the left side wheel or right side wheel is on the density type.
						minBumpHeight = 0.05,		-- (In meters) Minimum Bump height to generate
						maxBumpHeight = 0.07		-- (In meters) Maximum Bump height to generate
					},
					-- Sowed Density
					[2^g_currentMission.sowingChannel] = {
						defDropDis = 0.06,			-- (In meters) Default drop distance
						leftRightDropDis = 0,		-- Keep at 0. Only used my ploughChannel
						minBumpHeight = 0.005,		-- (In meters) Minimum Bump height to generate
						maxBumpHeight = 0.015		-- (In meters) Maximum Bump height to generate
					},
					-- Rootcrop Sowed Density
					[2^g_currentMission.sowingWidthChannel] = {
						defDropDis = 0.12,			-- (In meters) Default drop distance
						leftRightDropDis = 0,		-- Keep at 0. Only used my ploughChannel
						minBumpHeight = 0.04,		-- (In meters) Minimum Bump height to generate
						maxBumpHeight = 0.06		-- (In meters) Maximum Bump height to generate
					},
				},
				-- Soft terrain (Grass and dirt roads)
				[WheelsUtil.GROUND_SOFT_TERRAIN] = {
					defDropDis = 0.08,			-- (In meters) Default drop distance
					minBumpHeight = 0.01,		-- (In meters) Minimum Bump height to generate
					maxBumpHeight = 0.05		-- (In meters) Maximum Bump height to generate
				},
			}

		}

		-- Check if the vehicle have PloughingSpec
		if self.frontLeftWheel and self.frontLeftWheel.furrowDepth then
			self.realTerrain.hasPloughingSpec = true;
		end;

		local wheelIstracked = {};
		local i = 0;
		while true do
			local wheelnamei = string.format("vehicle.wheels.wheel(%d)", i);
			local reprStr = getXMLString(xmlFile, wheelnamei .. "#repr");
			if reprStr == nil then
				break;
			end;

			local tireType = getXMLString(xmlFile, wheelnamei .. "#tireType"); --tireType="crawler"
			table.insert(wheelIstracked, (tireType ~= nil and tireType == "crawler"));

			i = i + 1;
		end;

		-- Direction Node
		local DirectionNode;
		local isInverted = false;
		if self.aiTractorDirectionNode ~= nil then
			-- Tractor Direction Node
			DirectionNode = self.aiTractorDirectionNode;
		elseif self.aiTreshingDirectionNode ~= nil then
			-- Combine Direction Node
			DirectionNode = self.aiTreshingDirectionNode;
		elseif SpecializationUtil.hasSpecialization("attachable", self.specializations) then
			-- Trailer/Tool Direction Node
			DirectionNode = self.rootNode;

			-- Check if the node is in front of the attacher node
			local xTipper,yTipper,zTipper = getWorldTranslation(DirectionNode);
			local attacherNode = self.attacherJoint.node;
			local rxTemp, ryTemp, rzTemp = getRotation(attacherNode);
			setRotation(attacherNode, 0, 0, 0);
			local _,_,direction = worldToLocal(attacherNode, xTipper,yTipper,zTipper);
			setRotation(attacherNode, rxTemp, ryTemp, rzTemp);
			local isInFront = direction >= 0;

			-- Check if it's reversed based on if it's in front of the attacher node or not
			local x,y,z = getWorldTranslation(attacherNode);
			local _,_,tz = worldToLocal(node, x,y,z);
			isInverted = isInFront and (tz > 0) or (tz < 0);
		end;

		for id, wheel in ipairs(self.wheels) do
			wheel.RT = {};
			wheel.RT.isCrawler = wheelIstracked[id];
			wheel.RT.extraDrop = false;
			wheel.RT.fastUpdate = false;
			wheel.RT.origWheelRadius = wheel.radius;
			wheel.RT.oldWheelRadius = wheel.radius;

			if DirectionNode then
				local wx, wy, wz = getWorldTranslation(wheel.repr);
				local side,_,_ = worldToLocal(DirectionNode, wx, wy, wz);

				if (not isInverted and side > 0) or (isInverted and side < 0) then
					wheel.RT.isLeftWheel = true;
				else
					wheel.RT.isRightWheel = true;
				end;
			end;

			wheel.RT.lastX = 0;
			wheel.RT.lastY = 0;
			wheel.RT.lastZ = 0;
			wheel.RT.travelDistToUpdate = 1;
			wheel.RT.isOnBump = false;
			wheel.RT.radiusDelta = 0;
			wheel.RT.currentRadiusDelta = 0;
		end;
	end;
end;
	
function realTerrain:delete()
end;

function realTerrain:mouseEvent(posX, posY, isDown, isUp, button)
end;

function realTerrain:keyEvent(unicode, sym, modifier, isDown)
end;

function realTerrain:update(dt)
	-- We only need to run this code on the server.
	if self.isServer and self:getIsActive() and self.wheels and #self.wheels > 0 then
		--if realTerrain.printWheelInfo then
		--	print(realTerrainManager:tableShow(self.wheels, "self.wheels"));
		--	realTerrain.printWheelInfo = false;
		--end;

		-- Disable driveControl groundResponse if active. It will mess up our script.
		if g_currentMission.driveControl ~= nil and g_currentMission.driveControl.useModules.groundResponse then
			g_currentMission.driveControl.useModules.groundResponse = false;
			print("realTerrain: Disabling driveControl module groundResponse.");
		end;

		-- Only update if we are moving
		if self.movingDirection ~= 0 then
			-- Check if only one side is on ploughed areas.
			local ExtraDrop = false;
			local leftSide = false;
			local rightSide = false;
			for id, wheel in ipairs(self.wheels) do
				local wx, wy, wz = getWheelShapeContactPoint(wheel.node, wheel.wheelShape);
				if wx ~= nil then
					local density = getDensityAtWorldPos(g_currentMission.terrainDetailId, wx, wz) % 16;
					if density == 2^g_currentMission.ploughChannel then
						if wheel.RT.isLeftWheel then
							leftSide = true;
						elseif wheel.RT.isRightWheel then
							rightSide = true;
						end;
					end;
				end;
			end;
			if leftSide ~= rightSide then
				ExtraDrop = true;
			end;

			-- Update each wheel
			for id, wheel in ipairs(self.wheels) do
				-- Make sure we have this value already set befure using it in a check.
				if wheel.RT.currentDensity == nil then
					wheel.RT.currentDensity = 0;
				end;

				-- Get the world position of the wheel.
				local wx, wy, wz = getWheelShapeContactPoint(wheel.node, wheel.wheelShape);
				if wx ~= nil then
					local distance = Utils.vector3Length(wx - wheel.RT.lastX, wy - wheel.RT.lastY, wz - wheel.RT.lastZ);
					local density = getDensityAtWorldPos(g_currentMission.terrainDetailId, wx, wz) % 16;

					local isField = (density) > 0;
					local groundType = WheelsUtil.getGroundType(isField,false,wheel.lastColor)

					local needUpdate = distance >= wheel.RT.travelDistToUpdate or wheel.RT.currentDensity ~= density or self.realTerrain.extraDrop ~= ExtraDrop;

					----------------------------------------------------------------
					-- Change wheel radius if density have changed
					----------------------------------------------------------------
					if groundType == WheelsUtil.GROUND_FIELD then
						if wheel.RT.travelDistToUpdate == 10000 or needUpdate then
							if self.realTerrain.hasPloughingSpec and density ==(2^g_currentMission.ploughChannel) then
								-- Disable realTerrain on ploughChannel density if the vehicle has hasPloughingSpec
								realTerrain:resetWheelInfo(wheel, density);
							else
								local terrainInfo = self.realTerrain.terrainType[groundType][density];

								-- Set the radius Delta based on density level
								if terrainInfo then
									dropDis = terrainInfo.defDropDis;
									minBumpHeight = terrainInfo.minBumpHeight;
									maxBumpHeight = terrainInfo.maxBumpHeight;

									if self.realTerrain.hasPloughAttached and ExtraDrop then
										if density == 2^g_currentMission.ploughChannel then
											-- make it drop extra, but less bumpy
											wheel.RT.extraDrop = true;
											minBumpHeight = minBumpHeight * 0.1;
											maxBumpHeight = maxBumpHeight * 0.1;
										end;
										dropDis = dropDis + terrainInfo.leftRightDropDis;
									else
										wheel.RT.extraDrop = false;
									end;


									realTerrain:updateWheelInfo(groundType, wheel, density, needUpdate, dropDis, minBumpHeight, maxBumpHeight);
									-- No Density (Left in here just to be sure.)
								else
									realTerrain:resetWheelInfo(wheel);
								end;
							end;

							-- Update the saved position
							wheel.RT.lastX = wx;
							wheel.RT.lastY = wy;
							wheel.RT.lastZ = wz;

							if (wheel.RT.currentDensity ~= 2^g_currentMission.ploughChannel and wheel.RT.extraDrop)
							or (wheel.RT.currentDensity ~= density and wheel.RT.currentDensity == 2^g_currentMission.ploughChannel)
							or (wheel.RT.currentDensity == 2^g_currentMission.ploughChannel and self.realTerrain.extraDrop ~= ExtraDrop and (not ExtraDrop or wheel.RT.extraDrop)) then
								wheel.RT.fastUpdate = true;
							else
								wheel.RT.fastUpdate = false;
							end;

							-- Make sure we update to current density.
							wheel.RT.currentDensity = density;

						end;

					----------------------------------------------------------------
					-- Change wheel radius based on Soft terrain (Grass and dirt roads)
					----------------------------------------------------------------
					elseif groundType == WheelsUtil.GROUND_SOFT_TERRAIN then
						if wheel.RT.travelDistToUpdate == 10000 or needUpdate then
							local terrainInfo = self.realTerrain.terrainType[groundType];

							-- Set the radius Delta based on density level
							realTerrain:updateWheelInfo(groundType, wheel, density, needUpdate, terrainInfo.defDropDis, terrainInfo.minBumpHeight, terrainInfo.maxBumpHeight); -- Values needs tweaking.

							-- Update the saved position
							wheel.RT.lastX = wx;
							wheel.RT.lastY = wy;
							wheel.RT.lastZ = wz;

							if wheel.RT.currentDensity == 2^g_currentMission.ploughChannel and wheel.RT.extraDrop then
								wheel.RT.fastUpdate = true;
							else
								wheel.RT.fastUpdate = false;
							end;
							wheel.RT.extraDrop = false;

							wheel.RT.currentDensity = density;
						end;
					else
						realTerrain:resetWheelInfo(wheel);
					end;

					-- Update wheel
					if wheel.RT.currentRadiusDelta ~= wheel.RT.radiusDelta then
						-- Get current speed in km/h
						local currentSpeed = self.lastSpeedReal * 3600;
						-- Make speed multiplier (0 - 1 based on 0-80 km/h)
						local speedMultiplier = math.min(0.0125 * currentSpeed, 1);

						-- need to update faster if needed
						if wheel.RT.fastUpdate then
							speedMultiplier = speedMultiplier * 10;

						-- Need to update slower if on Soft terrain
						elseif groundType == WheelsUtil.GROUND_SOFT_TERRAIN and not wheel.RT.fastUpdate then
							speedMultiplier = speedMultiplier * 0.3;
						end;

						-- Make more smooth transition on bumps.
						if wheel.RT.currentRadiusDelta < wheel.RT.radiusDelta then
							wheel.RT.currentRadiusDelta = math.min(wheel.RT.radiusDelta, wheel.RT.currentRadiusDelta + ((dt / 1000) * speedMultiplier));
						else
							wheel.RT.currentRadiusDelta = math.max(wheel.RT.radiusDelta, wheel.RT.currentRadiusDelta - ((dt / 1000) * speedMultiplier));
						end;

						local collisionMask = 255 - 4; -- all up to bit 8, except bit 2 which is set by the players kinematic object
						local positionY = wheel.netInfo.yMin + wheel.netInfo.yRange;
						createWheelShape(wheel.node, wheel.positionX, positionY, wheel.positionZ, wheel.RT.origWheelRadius - wheel.RT.currentRadiusDelta, wheel.suspTravel, wheel.spring, wheel.damper, wheel.mass, collisionMask, wheel.wheelShape);

						-- Change wheel radius, so the track is placed in the right height.
						local newRadius = wheel.RT.origWheelRadius - wheel.RT.currentRadiusDelta
						if wheel.RT.oldWheelRadius ~= newRadius then
							wheel.radius = newRadius;
							wheel.RT.oldWheelRadius = wheel.radius;
							self.realTerrain.networkUpdate = true;
						end;
					end;
				else
					realTerrain:resetWheelInfo(wheel);
				end;
			end;

			self.realTerrain.extraDrop = ExtraDrop;
		end;
	end;
end;

function realTerrain:draw()
	-- TODO: (Satis) Remove this debug text render before release!
	if realTerrain.printDebug and self.isServer and self:getIsActive() and self.wheels and #self.wheels > 0 and (self.isSelected == nil or self.isSelected == true) then
		local textHeight = 0.02;
		local sectionHeight = #self.wheels * textHeight + (textHeight / 2);

		local offsetTop  = 0.85;
		local offsetLeft = 0.01;
		if self.isSelected then
			offsetLeft = 0.51;
		end

		renderText(offsetLeft, offsetTop, textHeight, string.format("Vehicle %s: Weight = %dkg", self.name, getMass(self.rootNode) * 1000));
		local calculatedWeight = 0;
		for id, wheel in ipairs(self.wheels) do
			-- Get the world position of the wheel.
			local wx, wy, wz = getWheelShapeContactPoint(wheel.node, wheel.wheelShape);
			if wx ~= nil then
				local distance = Utils.vector3Length(wx - wheel.RT.lastX, wy - wheel.RT.lastY, wz - wheel.RT.lastZ);
				local density = getDensityAtWorldPos(g_currentMission.terrainDetailId, wx, wz) % 16;
				renderText(offsetLeft, offsetTop - (textHeight * id), textHeight, string.format("Wheel %d: distance: %.2f, travelDistToUpdate: %.2f, x=%.1f, y=%.1f, z=%.1f", id, distance, wheel.RT.travelDistToUpdate, wheel.RT.lastX, wheel.RT.lastY, wheel.RT.lastZ));

				local isField = (density) > 0;
				local groundType = WheelsUtil.getGroundType(isField,false,wheel.lastColor)
				local wheelSide = "Center";
				if wheel.RT.isRightWheel then
					wheelSide = "Right";
				elseif wheel.RT.isLeftWheel then
					wheelSide = "Left";
				end;
				renderText(offsetLeft, offsetTop - sectionHeight - (textHeight * id), textHeight, string.format("Wheel %d: groundType: %s, Wheel Side: %s, fastUpdate: %s", id, groundTypes[groundType], wheelSide, tostring(wheel.RT.fastUpdate)));


				local ContactForce = getWheelShapeContactForce(wheel.node, wheel.wheelShape);
				local weight = ContactForce * 102;
				calculatedWeight = calculatedWeight + weight;
				renderText(offsetLeft, offsetTop - (sectionHeight * 2) - (textHeight * id), textHeight, string.format("Wheel %d: ContactForce = %.3f, Weight = %dkg, Wheel width = %dmm ", id, ContactForce, weight,  Utils.getNoNil(wheel.width * 1000, 0)));
			end;

		end;
		renderText(offsetLeft, offsetTop - (sectionHeight * 3) - textHeight, textHeight, string.format("Calculated Weight = %dkg", calculatedWeight));
	end;
end;

function realTerrain:readUpdateStream(streamId, timestamp, connection)
	if connection.isServer then
		local hasUpdate = streamReadBool(streamId);
		if hasUpdate then
			for id, wheel in ipairs(self.wheels) do
				wheel.radius = streamReadFloat32(streamId);
			end;
		end;
	end;
end;

function realTerrain:writeUpdateStream(streamId, connection, dirtyMask)
	if not connection.isServer then
		if streamWriteBool(streamId, (self.realTerrain and self.realTerrain.networkUpdate)) then
			self.realTerrain.networkUpdate = false;
			for id, wheel in ipairs(self.wheels) do
				streamWriteFloat32(streamId, wheel.radius);
			end;
		end;
	end;
end;

function realTerrain:detachImplement(implementIndex)
	if self.realTerrain and self.cruiseControl then
		local implement = self.attachedImplements[implementIndex];
		if implement.object ~= nil then
			local jointDesc = self.attacherJoints[implement.jointDescIndex];
			if jointDesc.RTMaxRotLimitBackup ~= nil then
				jointDesc.maxRotLimit = jointDesc.RTMaxRotLimitBackup;
				jointDesc.RTMaxRotLimitBackup = nil;
			end;
		end;
		self.realTerrain.isDirty = true;
	end;
end;

--- Function realTerrain:updateWheelInfo
-- @param groundType	int		The wheel object it self
-- @param wheel			Object	The wheel object it self
-- @param density		int		The density of the field
-- @param needUpdate	boolean	If it's an forsed update
-- @param radiusDelta	float	The default radiusDelta in meters
-- @param bumpMin		float	Bump min height in meters
-- @param bumpMax		float	Bump max height in meters
function realTerrain:updateWheelInfo(groundType, wheel, density, needUpdate, radiusDelta, bumpMin, bumpMax)
	local minMax = math.random(bumpMin * 1000, bumpMax * 1000);
	if groundType == WheelsUtil.GROUND_FIELD then
		if wheel.RT.currentDensity ~= density or (needUpdate and wheel.RT.isOnBump) then
			wheel.RT.radiusDelta = radiusDelta;
			wheel.RT.isOnBump = false;
			wheel.RT.travelDistToUpdate = math.random(3, 20) / 10;
		else
			wheel.RT.radiusDelta = radiusDelta - minMax / 1000;
			wheel.RT.isOnBump = true;
			wheel.RT.travelDistToUpdate = minMax / 50;
		end;
	elseif groundType == WheelsUtil.GROUND_SOFT_TERRAIN then
		if wheel.RT.isOnBump then
			wheel.RT.radiusDelta = radiusDelta;
			wheel.RT.isOnBump = false;
			wheel.RT.travelDistToUpdate = math.random(3, 20) / 3;
		else
			wheel.RT.radiusDelta = radiusDelta - minMax / 1000;
			wheel.RT.isOnBump = true;
			wheel.RT.travelDistToUpdate = minMax / 15;
		end;
	end;

	if groundType == WheelsUtil.GROUND_FIELD or groundType == WheelsUtil.GROUND_SOFT_TERRAIN then
		-- Crawlers have a bigger surface, so they should not go down that deep. (Only around 30% of normal)
		if wheel.RT.isCrawler then
			wheel.RT.radiusDelta = math.max(0, wheel.RT.radiusDelta * 0.3);
		end;

		-- Make sure small wheels don't bounce and dig too much into the ground.
		if not wheel.RT.isCrawler and wheel.RT.origWheelRadius < 0.5 then
			if wheel.RT.origWheelRadius > 0.25 then
				-- if the wheel diameter is between 0.25 and 0.5 meter, Graduatly reduce the bumping.
				local multiplier = ((1 / 25) * ((wheel.RT.origWheelRadius - 0.25) * 75)) + 0.25;
				wheel.RT.radiusDelta = math.max(0, wheel.RT.radiusDelta * multiplier);
			else
				-- If the wheel diameter is less than 0.25 meter, don't add any wobling.
				wheel.RT.radiusDelta = 0;
			end;
		end;
	end;
end;

function realTerrain:resetWheelInfo(wheel, density)
	wheel.RT.radiusDelta = 0;
	wheel.RT.isOnBump = false;
	wheel.RT.travelDistToUpdate = 10000; -- Set it to 10 km, since we don't need to force update until on another density.

	wheel.RT.fastUpdate = false;
	wheel.RT.currentDensity = Utils.getNoNil(density, 0); -- If density is set, it will prevent further update until the density change to another density than the one provided.
end;

function realTerrain:toggleDebugRender()
	realTerrain.printDebug = not realTerrain.printDebug;
end;

--- Function realTerrain:hasPloughAttached
-- Check if there is an plough attached to the vehicle
function realTerrain:hasPloughAttached()
	local hasPloughAttached = false;
	for index, implement in ipairs(self.attachedImplements) do
		if SpecializationUtil.hasSpecialization(Plough, implement.object.specializations) then
			hasPloughAttached = true;
		end;
	end;

	return hasPloughAttached;
end;

-------------------------------------------------------------------
-- Overwrite Vehicle:attachImplement() for pre setting maxRotLimit.
-------------------------------------------------------------------
--- Make sure this overwrite script is only runned once in case some other mods also uses the realTerrain.lua
if not Vehicle.attachImplementOverwritenByRealTerrain then
	Vehicle.attachImplementOverwritenByRealTerrain = true;

	local OriginalVehicleAttachImplement = Vehicle.attachImplement;
	Vehicle.attachImplement = function(self, object, inputJointDescIndex, jointDescIndex, noEventSend, index, startLowered)
		if self.realTerrain and self.cruiseControl then
			if SpecializationUtil.hasSpecialization(Plough, object.specializations) then
				self.realTerrain.hasPloughAttached = true;

				local jointDesc = self.attacherJoints[jointDescIndex];
				if jointDesc.jointType == Vehicle.jointTypeNameToInt["implement"] then
					jointDesc.RTMaxRotLimitBackup = jointDesc.maxRotLimit;
					jointDesc.maxRotLimit[1] = math.rad(5);
				end;
			end;
		end;

		------------------------------------------------------------------------
		-- Run the original script
		------------------------------------------------------------------------
		OriginalVehicleAttachImplement(self, object, inputJointDescIndex, jointDescIndex, noEventSend, index, startLowered);
	end;
end;

