--
-- Corrodible
--
-- @author:		JoPi
-- @date:		23/12/21
-- @version:	v1.0
-- @history:	v1.0 - initial version
--
-- free for non-commercial usage

Corrodible = {};
Corrodible.MOD_NAME = g_currentModName;

function Corrodible.prerequisitesPresent(specializations)
	return SpecializationUtil.hasSpecialization(Wearable, specializations);
end;

function Corrodible.initSpecialization()
	Vehicle.xmlSchema:setXMLSpecializationType("Corrodible");
	Vehicle.xmlSchema:register(XMLValueType.FLOAT, "vehicle.corrodible#corrosionDuration", "Duration until fully corroded (minutes)");
	Vehicle.xmlSchema:register(XMLValueType.FLOAT, "vehicle.corrodible#rainMultiplier", "Multiplier while raining", 3);
	Vehicle.xmlSchema:register(XMLValueType.BOOL, "vehicle.corrodible#canReapplyWear", "Allow partial corrosion reduction", false);
	Vehicle.xmlSchema:register(XMLValueType.FLOAT, "vehicle.corrodible#wearReapplicationMultiplier", "Corrosion reduction multiplier (based on 'wearDuration')", 50);
	Vehicle.xmlSchema:setXMLSpecializationType();
	Vehicle.xmlSchemaSavegame:register(XMLValueType.FLOAT, ("vehicles.vehicle(?).%s.corrodible.corrosionNode(?)#amount01"):format(Corrodible.MOD_NAME), "Corrosion amount");
	Vehicle.xmlSchemaSavegame:register(XMLValueType.FLOAT, ("vehicles.vehicle(?).%s.corrodible.corrosionNode(?)#amount02"):format(Corrodible.MOD_NAME), "Corrosion reduction amount");
end;

function Corrodible.registerFunctions(vehicleType)
	SpecializationUtil.registerFunction(vehicleType, "updateCorrosionAmounts", Corrodible.updateCorrosionAmounts);
	SpecializationUtil.registerFunction(vehicleType, "getUsageCausesCorrosion", Corrodible.getUsageCausesCorrosion);
	SpecializationUtil.registerFunction(vehicleType, "getNonUsageCausesCorrosion", Corrodible.getNonUsageCausesCorrosion);
	SpecializationUtil.registerFunction(vehicleType, "getIsCausingWearReapplication", Corrodible.getIsCausingWearReapplication);
	SpecializationUtil.registerFunction(vehicleType, "addCorrosionAmounts", Corrodible.addCorrosionAmounts);
	SpecializationUtil.registerFunction(vehicleType, "setNodeCorrosionAmounts", Corrodible.setNodeCorrosionAmounts);
	SpecializationUtil.registerFunction(vehicleType, "getNodeCorrosionAmounts", Corrodible.getNodeCorrosionAmounts);
	SpecializationUtil.registerFunction(vehicleType, "getCorrosionMultiplier", Corrodible.getCorrosionMultiplier);
	SpecializationUtil.registerFunction(vehicleType, "getCanReapplyWear", Corrodible.getCanReapplyWear);
	SpecializationUtil.registerFunction(vehicleType, "getWearReapplicationMultiplier", Corrodible.getWearReapplicationMultiplier);
end;

function Corrodible.registerOverwrittenFunctions(vehicleType)
	SpecializationUtil.registerOverwrittenFunction(vehicleType, "setNodeWearAmount", Corrodible.setNodeWearAmount);
	SpecializationUtil.registerOverwrittenFunction(vehicleType, "addToGlobalWearableNode", Corrodible.addToGlobalWearableNode);
	SpecializationUtil.registerOverwrittenFunction(vehicleType, "addToLocalWearableNode", Corrodible.addToLocalWearableNode);
end;

function Corrodible.registerEventListeners(vehicleType)
	SpecializationUtil.registerEventListener(vehicleType, "onLoad", Corrodible);
	SpecializationUtil.registerEventListener(vehicleType, "onLoadFinished", Corrodible);
	SpecializationUtil.registerEventListener(vehicleType, "onReadStream", Corrodible);
	SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", Corrodible);
	SpecializationUtil.registerEventListener(vehicleType, "onReadUpdateStream", Corrodible);
	SpecializationUtil.registerEventListener(vehicleType, "onWriteUpdateStream", Corrodible);
	SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", Corrodible);
end;

function Corrodible:onLoad(savegame)
	local spec = Corrodible.getSpecTable(self, "corrodible");
	spec.corrosionDuration = self.xmlFile:getValue("vehicle.corrodible#corrosionDuration", 0) * 60 * 1000;
	if spec.corrosionDuration ~= 0 then
		spec.corrosionDuration = 1 / spec.corrosionDuration;
	end;
	spec.corrosionDuration = math.min(spec.corrosionDuration, Corrodible.getSpecTable(self, "wearable").wearDuration / 1.25);
	spec.rainMultiplier = self.xmlFile:getValue("vehicle.corrodible#rainMultiplier", 3);
	spec.canReapplyWear = self.xmlFile:getValue("vehicle.corrodible#canReapplyWear", false);
	spec.wearReapplicationMultiplier = self.xmlFile:getValue("vehicle.corrodible#wearReapplicationMultiplier", 50);
	spec.dirtyFlag = self:getNextDirtyFlag();
end;

function Corrodible:onLoadFinished(savegame)
	if savegame ~= nil then
		local spec = Corrodible.getSpecTable(self, "wearable");
		if spec.wearableNodes ~= nil then
			for i, nodeData in ipairs(spec.wearableNodes) do
				if nodeData.isValidCorrosionNode then
					local nodeKey = ("%s.%s.corrodible.corrosionNode(%d)"):format(savegame.key, Corrodible.MOD_NAME, i - 1);
					self:setNodeCorrosionAmounts(nodeData, savegame.xmlFile:getValue(nodeKey .. "#amount01", 0), savegame.xmlFile:getValue(nodeKey .. "#amount02", 0), true);
				end;
			end;
		end;
	end;
end;

function Corrodible:saveToXMLFile(xmlFile, key, usedModNames)
	local spec = Corrodible.getSpecTable(self, "wearable");
	if spec.wearableNodes ~= nil then
		for i, nodeData in ipairs(spec.wearableNodes) do
			if nodeData.isValidCorrosionNode then
				local nodeKey = ("%s.corrosionNode(%d)"):format(key, i - 1);
				local corrosionAmount01, corrosionAmount02 = self:getNodeCorrosionAmounts(nodeData);
				xmlFile:setValue(nodeKey .. "#amount01", corrosionAmount01);
				xmlFile:setValue(nodeKey .. "#amount02", corrosionAmount02);
			end;
		end;
	end;
end;

function Corrodible:onReadStream(streamId, connection)
	local spec = Corrodible.getSpecTable(self, "wearable");
	if spec.wearableNodes ~= nil then
		for _,nodeData in ipairs(spec.wearableNodes) do
			if nodeData.isValidCorrosionNode then
				local corrosionAmount01 = 0;
				local corrosionAmount02 = 0;
				if streamReadBool(streamId) then
					corrosionAmount01 = streamReadUIntN(streamId, Wearable.SEND_NUM_BITS) / Wearable.SEND_MAX_VALUE;
					if self:getCanReapplyWear() then
						corrosionAmount02 = streamReadUIntN(streamId, Wearable.SEND_NUM_BITS) / Wearable.SEND_MAX_VALUE;
					end;
				end;
				self:setNodeCorrosionAmounts(nodeData, corrosionAmount01, corrosionAmount02, true);
			end;
		end;
	end;
end;

function Corrodible:onWriteStream(streamId, connection)
	local spec = Corrodible.getSpecTable(self, "wearable");
	if spec.wearableNodes ~= nil then
		for _,nodeData in ipairs(spec.wearableNodes) do
			if nodeData.isValidCorrosionNode and streamWriteBool(streamId, self:getNodeWearAmount(nodeData) > 0) then
				local corrosionAmount01, corrosionAmount02 = self:getNodeCorrosionAmounts(nodeData);
				streamWriteUIntN(streamId, math.floor(corrosionAmount01 * Wearable.SEND_MAX_VALUE + 0.5), Wearable.SEND_NUM_BITS);
				if self:getCanReapplyWear() then
					streamWriteUIntN(streamId, math.floor(corrosionAmount02 * Wearable.SEND_MAX_VALUE + 0.5), Wearable.SEND_NUM_BITS);
				end;
			end;
		end;
	end;
end;

function Corrodible:onReadUpdateStream(streamId, timestamp, connection)
	if connection:getIsServer() then
		local spec = Corrodible.getSpecTable(self, "wearable");
		if spec.wearableNodes ~= nil and streamReadBool(streamId) then
			for _,nodeData in ipairs(spec.wearableNodes) do
				if nodeData.isValidCorrosionNode then
					local corrosionAmount01 = 0;
					local corrosionAmount02 = 0;
					if streamReadBool(streamId) then
						corrosionAmount01 = streamReadUIntN(streamId, Wearable.SEND_NUM_BITS) / Wearable.SEND_MAX_VALUE;
						if self:getCanReapplyWear() then
							corrosionAmount02 = streamReadUIntN(streamId, Wearable.SEND_NUM_BITS) / Wearable.SEND_MAX_VALUE;
						end;
					end;
					self:setNodeCorrosionAmounts(nodeData, corrosionAmount01, corrosionAmount02, true);
				end;
			end;
		end;
	end;
end;

function Corrodible:onWriteUpdateStream(streamId, connection, dirtyMask)
	if not connection:getIsServer() then
		local spec = Corrodible.getSpecTable(self, "wearable");
		if spec.wearableNodes ~= nil and streamWriteBool(streamId, bitAND(dirtyMask, Corrodible.getSpecTable(self, "corrodible").dirtyFlag) ~= 0) then
			for _,nodeData in ipairs(spec.wearableNodes) do
				if nodeData.isValidCorrosionNode and streamWriteBool(streamId, self:getNodeWearAmount(nodeData) > 0) then
					local corrosionAmount01, corrosionAmount02 = self:getNodeCorrosionAmounts(nodeData);
					streamWriteUIntN(streamId, math.floor(corrosionAmount01 * Wearable.SEND_MAX_VALUE + 0.5), Wearable.SEND_NUM_BITS);
					if self:getCanReapplyWear() then
						streamWriteUIntN(streamId, math.floor(corrosionAmount02 * Wearable.SEND_MAX_VALUE + 0.5), Wearable.SEND_NUM_BITS);
					end;
				end;
			end;
		end;
	end;
end;

function Corrodible:onUpdateTick(dt, isActive, isActiveForInput, isSelected)
	if self.isServer then
		local spec = Corrodible.getSpecTable(self, "wearable");
		if spec.wearableNodes ~= nil then
			for _,nodeData in ipairs(spec.wearableNodes) do
				if nodeData.isValidCorrosionNode then
					local nonUsageCausesCorrosion = self:getNonUsageCausesCorrosion();
					if self:getUsageCausesCorrosion() or nonUsageCausesCorrosion then
						local wearAmount = self:getNodeWearAmount(nodeData);
						if wearAmount > 0 then
							local changedCorrosionAmount01, changedCorrosionAmount02 = nodeData.corrosionUpdateFunc(self, nodeData, dt);
							if changedCorrosionAmount01 ~= 0 or changedCorrosionAmount02 ~= 0 then
								local corrosionAmount01, corrosionAmount02 = self:getNodeCorrosionAmounts(nodeData);
								self:setNodeCorrosionAmounts(nodeData, corrosionAmount01 + changedCorrosionAmount01, corrosionAmount02 + changedCorrosionAmount02);
							end;
							if nonUsageCausesCorrosion then
								local corrosionAmount01, corrosionAmount02 = self:getNodeCorrosionAmounts(nodeData);
								if corrosionAmount01 < wearAmount or corrosionAmount02 < corrosionAmount01 then
									self:raiseActive();
								end;
							end;
						end;
					end;
				end;
			end;
		end;
	end;
end;

function Corrodible:updateCorrosionAmounts(nodeData, dt)
	local changedCorrosionAmount01 = dt * Corrodible.getSpecTable(self, "corrodible").corrosionDuration * self:getCorrosionMultiplier() * Corrodible.getCorrosionIntervalMultiplier();
	local changedCorrosionAmount02 = changedCorrosionAmount01;
	if self:getCanReapplyWear() and self:getIsCausingWearReapplication() then
		changedCorrosionAmount02 = -dt * Corrodible.getSpecTable(self, "wearable").wearDuration * self:getWearMultiplier() * self:getWearReapplicationMultiplier() * Corrodible.getWearReapplicationIntervalMultiplier();
	end;
	return changedCorrosionAmount01, changedCorrosionAmount02;
end;

function Corrodible:getUsageCausesCorrosion()
	return true;
end;

function Corrodible:getNonUsageCausesCorrosion()
	return true;
end;

function Corrodible:getIsCausingWearReapplication()
	for _,specName in ipairs({"cultivator", "fruitPreparer", "mulcher", "plow", "roller", "sowingMachine", "stonePicker", "weeder"}) do
		local spec = Corrodible.getSpecTable(self, specName);
		if spec ~= nil and spec.isWorking then
			return true;
		end;
	end;
	return false;
end;

function Corrodible:addCorrosionAmounts(addedCorrosionAmount01, addedCorrosionAmount02, ...)
	local spec = Corrodible.getSpecTable(self, "wearable");
	if spec.wearableNodes ~= nil then
		for _,nodeData in ipairs(spec.wearableNodes) do
			if nodeData.isValidCorrosionNode then
				local corrosionAmount01, corrosionAmount02 = self:getNodeCorrosionAmounts(nodeData);
				self:setNodeCorrosionAmounts(nodeData, corrosionAmount01 + Utils.getNoNil(addedCorrosionAmount01, 0), corrosionAmount02 + Utils.getNoNil(addedCorrosionAmount02, 0), ...);
			end;
		end;
	end;
end;

function Corrodible:setNodeCorrosionAmounts(nodeData, corrosionAmount01, corrosionAmount02, force)
	nodeData.corrosionAmount01 = MathUtil.clamp(Utils.getNoNil(corrosionAmount01, nodeData.corrosionAmount01), 0, self:getNodeWearAmount(nodeData));
	nodeData.corrosionAmount02 = MathUtil.clamp(Utils.getNoNil(corrosionAmount02, nodeData.corrosionAmount02), self:getCanReapplyWear() and 0 or nodeData.corrosionAmount01, nodeData.corrosionAmount01);
	if math.abs(nodeData.corrosionAmount01Sent - nodeData.corrosionAmount01) > Wearable.SEND_THRESHOLD or math.abs(nodeData.corrosionAmount02Sent - nodeData.corrosionAmount02) > Wearable.SEND_THRESHOLD or force then
		for _,node in pairs(nodeData.nodes) do
			if getHasShaderParameter(node, "rustControl") then
				local _, _, z, w = getShaderParameter(node, "rustControl");
				setShaderParameter(node, "rustControl", nodeData.corrosionAmount01, 1 - nodeData.corrosionAmount02, z, w, false);
			end;
		end;
		if self.isServer then
			self:raiseDirtyFlags(Corrodible.getSpecTable(self, "corrodible").dirtyFlag);
			nodeData.corrosionAmount01Sent = nodeData.corrosionAmount01;
			nodeData.corrosionAmount02Sent = nodeData.corrosionAmount02;
		end;
	end;
end;

function Corrodible:getNodeCorrosionAmounts(nodeData)
	return nodeData.corrosionAmount01, nodeData.corrosionAmount02;
end;

function Corrodible:getCorrosionMultiplier()
	if g_currentMission.environment.weather:getRainFallScale() > 0.1 and g_currentMission.environment.weather:getTimeSinceLastRain() < 20 then
		local isAffectedByRain = true;
		if g_currentMission.indoorMask ~= nil and g_currentMission.indoorMask:hasMask() then
			local wx,_,wz = getWorldTranslation(self.rootNode);
			local posX = wx - self.size.width / 2 + self.size.widthOffset;
			local posZ = wz - self.size.length / 2 + self.size.lengthOffset;
			g_currentMission.indoorMask:setParallelogramUVCoords(g_currentMission.indoorMask.modifierValue, posX, posZ, posX + self.size.width, posZ, posX, posZ + self.size.length);
			isAffectedByRain = g_currentMission.indoorMask.modifierValue:executeGet() == 0;
		end;
		if isAffectedByRain then
			return Corrodible.getSpecTable(self, "corrodible").rainMultiplier;
		end;
	end;
	return 1;
end;

function Corrodible:getCanReapplyWear()
	return Corrodible.getSpecTable(self, "corrodible").canReapplyWear;
end;

function Corrodible:getWearReapplicationMultiplier()
	return Corrodible.getSpecTable(self, "corrodible").wearReapplicationMultiplier;
end;

function Corrodible:setNodeWearAmount(superFunc, nodeData, wearAmount, ...)
	superFunc(self, nodeData, wearAmount, ...);
	if nodeData.isValidCorrosionNode then
		self:setNodeCorrosionAmounts(nodeData, nil, nil, ...);
	end;
end;

function Corrodible:addToGlobalWearableNode(superFunc, node, ...)
	superFunc(self, node, ...);
	local nodeData = Corrodible.getSpecTable(self, "wearable").wearableNodes[1];
	if nodeData ~= nil then
		nodeData.isValidCorrosionNode = nodeData.isValidCorrosionNode or node ~= nil and node ~= 0 and getHasShaderParameter(node, "rustControl");
	end;
end;

function Corrodible:addToLocalWearableNode(superFunc, node, updateFunc, customIndex, extraParams, ...)
	extraParams = Utils.getNoNil(extraParams, {});
	extraParams.corrosionUpdateFunc = Corrodible.updateCorrosionAmounts;
	extraParams.corrosionAmount01 = 0;
	extraParams.corrosionAmount01Sent = 0;
	extraParams.corrosionAmount02 = 0;
	extraParams.corrosionAmount02Sent = 0;
	extraParams.isValidCorrosionNode = extraParams.isValidCorrosionNode or node ~= nil and node ~= 0 and getHasShaderParameter(node, "rustControl");
	superFunc(self, node, updateFunc, customIndex, extraParams, ...);
end;

function Corrodible.getCorrosionIntervalMultiplier()
	return 0.5;
end;

function Corrodible.getWearReapplicationIntervalMultiplier()
	return 0.5;
end;

function Corrodible.getSpecTable(vehicle, specName)
	return Utils.getNoNil(vehicle[("spec_%s.%s"):format(Corrodible.MOD_NAME, specName)], vehicle[("spec_%s"):format(specName)]);
end;