--
-- Pitchfork
-- Specialization for Pitchfork
--
-- @author  	Burner
-- @version 	v1.0
-- @date  		28.06.2012
--

pitchforkListener = {};

function pitchforkListener:loadMap(name)
  self.pitchforks = {};
end;

function pitchforkListener:deleteMap()
end;

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

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

function pitchforkListener:update(dt)
end;

function pitchforkListener:draw()
	for i=1, table.getn(self.pitchforks) do
		if self.pitchforks[i].isEntered then
			self.pitchforks[i]:draw();
		end;
	end;
end;

addModEventListener(pitchforkListener);

Pitchfork = {};

function Pitchfork.initSpecialization()
	Vehicle.registerJointType("handtool");
end;

function Pitchfork.prerequisitesPresent(specializations)
	return true;
end;
	
function Pitchfork:load(xmlFile)
	self.attachBale = SpecializationUtil.callSpecializationsFunction("attachBale");

	self.findManureTriggerRaycastCallback = Pitchfork.findManureTriggerRaycastCallback;
	self.manureTrigger = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.manureTrigger#index"));

	self.getPlayerInRange = Pitchfork.getPlayerInRange;
	self.setActiveState = SpecializationUtil.callSpecializationsFunction("setActiveState");
	self.stickPitchfork = SpecializationUtil.callSpecializationsFunction("stickPitchfork");
	self.setPitchforkActive = SpecializationUtil.callSpecializationsFunction("setPitchforkActive");
	self.setPitchforkDischarge = SpecializationUtil.callSpecializationsFunction("setPitchforkDischarge");
	self.drawGrainLevel = SpecializationUtil.callSpecializationsFunction("drawGrainLevel");
	
	self.trailerTrigger = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.trailerTrigger#index"));
	self.baleTrigger = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.baleTrigger#index"));
	
	self.pitchfork = {};
	self.pitchfork.actionDistance = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.pitchfork#actionDistance"), 5);

	self.pitchfork.rootNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.pitchfork#rootNode"));
	self.pitchfork.node = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.pitchfork#node"));
	
	local x,y,z = getTranslation(self.pitchfork.rootNode);	
	self.pitchfork.rootNodeTrans = {x,y,z};
	x,y,z = getRotation(self.pitchfork.rootNode);
	self.pitchfork.rootNodeRot = {x,y,z};
	
	self.isPlayerInRange = false;
	self.controllingPlayer = nil;
	self.isPitchforkActive = false;
	self.isPitchforkDischarge = false;
	self.isSticked = false;
	self.playerIdBackup = nil;
	self.hasPlayerQuit = false;
	
	self.fillScale = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.fillScale#value"), 1);
	self.unloadingCapacity = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.unloadingCapacity"), 10);
	
	self.placeArrow = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.placeArrow#index"));
	local displayArrow = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.placeArrow#display"), false);
	if self.placeArrow ~= nil and displayArrow then
		link(getRootNode(), self.placeArrow);
	end;
	
	self.unloadingParticleSystems = {};
	local i = 0;
	while true do
		local key = string.format("vehicle.unloadingParticleSystems.unloadingParticleSystem(%d)", i);
		local t = getXMLString(xmlFile, key .. "#type");
		if t == nil then
			break;
		end;
		local desc = Fillable.fillTypeNameToInt[t];
		if desc ~= nil then
			local fillType = desc;
			local currentPS = {};

			local particleNode = Utils.loadParticleSystem(xmlFile, currentPS, key, self.components, false, "$data/vehicles/particleSystems/trailerDischargeParticleSystem.i3d", self.baseDirectory);
			self.unloadingParticleSystems[fillType] = currentPS;
			if self.defaultdischargeParticleSystems == nil then
				self.defaultdischargeParticleSystems = currentPS;
			end;
		end;
		i = i + 1;
	end;
	
	self.loadToTrailerFillTypes = {};
	self.loadToTrailerFillTypes[Fillable.FILLTYPE_UNKNOWN] = true;
	local loadToTrailerFillTypes = getXMLString(xmlFile, "vehicle.loadToTrailerFillTypes#fruitTypes");
	if loadToTrailerFillTypes ~= nil then
		local types = Utils.splitString(" ", loadToTrailerFillTypes);
		for k,v in pairs(types) do
			local collectType = Fillable.fillTypeNameToInt[v];
			if collectType ~= nil then
				self.loadToTrailerFillTypes[collectType] = true;
			end;
		end;
	end;
	
	self.canLoadToTrailer = false;
	self.trailerFoundId = nil;
	self.lastTrailerFoundId = nil;
	
	self.hudBasePosX = 0.7543;
    self.hudBasePosY = 0.01238;
    self.hudBaseWidth = 0.2371;
    self.hudBaseHeight = 0.1581;

    self.hudBarWidth = 0.205;
    self.hudBarHeight = 0.02190;
    self.hudBarOffsetX = 0.023571;
    self.hudBarStartOffsetY = 0.0085714;
    self.hudBarOffsetY = 0.0395;

    self.hudBackgroundOverlay = Overlay:new("hudBackgroundOverlay", "dataS2/menu/hud/vehicleHUD_background.png", self.hudBasePosX, self.hudBasePosY, self.hudBaseWidth, self.hudBaseHeight);
    self.hudFramesOverlay = Overlay:new("hudFramesOverlay", "dataS2/menu/hud/vehicleHUD_frames.png", self.hudBasePosX, self.hudBasePosY, self.hudBaseWidth, self.hudBaseHeight);

    self.hudBarGreenOverlay = Overlay:new("hudBarGreenOverlay", "dataS2/menu/hud/vehicleHUD_barGreen.png", self.hudBasePosX + self.hudBarOffsetX, self.hudBasePosY + self.hudBarStartOffsetY, self.hudBarWidth, self.hudBarHeight);

    self.hudBarGoldOverlay = Overlay:new("hudBarGoldOverlay", "dataS2/menu/hud/vehicleHUD_barGold.png", self.hudBasePosX + self.hudBarOffsetX, self.hudBasePosY + self.hudBarStartOffsetY + 1 * self.hudBarOffsetY, self.hudBarWidth, self.hudBarHeight);

    self.hudBarRedOverlay = Overlay:new("hudBarRedOverlay", "dataS2/menu/hud/vehicleHUD_barRed.png", self.hudBasePosX + self.hudBarOffsetX, self.hudBasePosY + self.hudBarStartOffsetY + 3 * self.hudBarOffsetY, self.hudBarWidth, self.hudBarHeight);
	
	self.mountedBaleId = nil;
	self.isEntered = false;
	
	self.fallOut = true;
	self.isShovel = true;
	self.unloadingTipTrigger = nil;
	
	local path = Utils.getFilename("overlay.png", self.baseDirectory);
    self.unloadingOverlay = Overlay:new("hudPDAControl", path, g_currentMission.fruitSymbolX, g_currentMission.fruitSymbolY-0.11, g_currentMission.fruitSymbolSize, g_currentMission.fruitSymbolSize * (4 / 3)); 

	self.renderUnloadOverlay = false;
	
	table.insert(pitchforkListener.pitchforks,self);
end;

function Pitchfork:delete()
	self:attachBale(nil);
	if self.placeArrow ~= nil then
		link(self.rootNode, self.placeArrow);
	end;
	self:setActiveState(nil, true);
	if self.hudBackgroundOverlay then
		self.hudBackgroundOverlay:delete();
	end;
	if self.hudFramesOverlay then
		self.hudFramesOverlay:delete();
	end;
	for _, particleSystem in pairs(self.unloadingParticleSystems) do
		Utils.deleteParticleSystem(particleSystem);
	end;
end;

function Pitchfork:readStream(streamId, connection)
	local id = streamReadInt32(streamId);
	if id ~= -1 then
		self.playerIdBackup = id;
	end;
	
	self:stickPitchfork(streamReadBool(streamId), true);
	self:setPitchforkActive(streamReadBool(streamId), true);
	self:setPitchforkDischarge(streamReadBool(streamId), true);
	self:attachBale(streamReadInt32(streamId), true);
end;

function Pitchfork:writeStream(streamId, connection)
	local idToWrite = -1;
	if self.controllingPlayer ~= nil then
		idToWrite = networkGetObjectId(self.controllingPlayer);
	end;
	streamWriteInt32(streamId, idToWrite);
	
	streamWriteBool(streamId, self.isSticked);
	streamWriteBool(streamId, self.isPitchforkActive);
	streamWriteBool(streamId, self.isPitchforkDischarge);
	streamWriteInt32(streamId, self.mountedBaleId);
end;

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

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

function Pitchfork:update(dt)
	if self.placeArrow ~= nil then
		setVisibility(self.placeArrow, (self.controllingPlayer == nil and self.attacherVehicle == nil));
		local x,y,z = getWorldTranslation(self.pitchfork.node);
		setTranslation(self.placeArrow, x,y+1.3,z);
	end;
	
	if g_gui.currentGui ~= nil and self.playerIdBackup ~= nil then	
		self:setActiveState(networkGetObject(self.playerIdBackup), true);
		self.playerIdBackup = nil;		
	end;
	
	if self.controllingPlayer == g_currentMission.player or (self.controllingPlayer == nil and self.isPlayerInRange) then	
		if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA) then
			if self.attacherVehicle ~= nil then
				self.attacherVehicle:detachImplementByObject(self);
			end;
			if self.controllingPlayer == g_currentMission.player then
				self:setActiveState(nil);
			else
				if self.isSticked then
					self:stickPitchfork(false);
				end;
				self:setActiveState(g_currentMission.player);
			end;
		end;	
		
		if self.controllingPlayer ~= nil and self.controllingPlayer == g_currentMission.player then	
			-- update pitchfork position --
			local x,y,z = getWorldTranslation(self.controllingPlayer.graphicsRootNode);
			setTranslation(self.pitchfork.rootNode, x,y - 0.5,z);
			
			local x,y,z = getWorldRotation(self.controllingPlayer.graphicsRootNode);
			setRotation(self.pitchfork.rootNode, x,y,z);
			
			if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA2) then
				self:stickPitchfork(true);
				self:setActiveState(nil);
			end;
			
			g_currentMission:addHelpButtonText(string.format(g_i18n:getText("pitchfork_stick")), InputBinding.IMPLEMENT_EXTRA2);
			g_currentMission:addHelpButtonText(string.format(g_i18n:getText("pitchfork_put_down")), InputBinding.IMPLEMENT_EXTRA);
			
			if self.fillLevel < self.capacity and not self.isPitchforkDischarge then
				g_currentMission:addExtraPrintText(g_i18n:getText("pitchfork_use"));
				if Input.isMouseButtonPressed(Input.MOUSE_BUTTON_LEFT) and g_gui.currentGui == nil then
					if not self.isPitchforkActive then
						self:setPitchforkActive(true);
					end;
				else
					if self.isPitchforkActive then
						self:setPitchforkActive(false);
					end;
				end;
			end;
			if self.fillLevel > 0 and not self.isPitchforkActive then
				if self.canLoadToTrailer then
					g_currentMission:addExtraPrintText(g_i18n:getText("pitchfork_discharge_to_trailer"));
				else
					g_currentMission:addExtraPrintText(g_i18n:getText("pitchfork_discharge"));
				end;
				if Input.isMouseButtonPressed(Input.MOUSE_BUTTON_RIGHT) and g_gui.currentGui == nil then
					if not self.isPitchforkDischarge then
						self:setPitchforkDischarge(true);
					end;
				else
					if self.isPitchforkDischarge then
						self:setPitchforkDischarge(false);
					end;
				end;
			end;
		elseif self.controllingPlayer == nil and self.isPlayerInRange then
			g_currentMission:addHelpButtonText(string.format(g_i18n:getText("pitchfork_take")), InputBinding.IMPLEMENT_EXTRA);	
		end;
	else
		if self.controllingPlayer ~= nil then	
			if g_currentMission:getIsServer() then
				if g_currentMission.connectionsToPlayer[self.controllingPlayer.creatorConnection] == nil then
					self.hasPlayerQuit = true;
					self:setActiveState(nil);
				end;
			end;

			if self.controllingPlayer ~= nil then
				local x,y,z = getWorldTranslation(self.controllingPlayer.rootNode);
				setTranslation(self.pitchfork.rootNode, x,y-0.5,z);
				local x,y,z = getRotation(self.controllingPlayer.graphicsRootNode);
				setRotation(self.pitchfork.rootNode, x,y,z);
			end;
		end;
	end;
	
	self.trailerFoundId = nil;
	self.canLoadToTrailer = false;
	self.unloadingTipTrigger = nil;
	self.manureTriggerFound = 0;
	self.renderUnloadOverlay = false;
	
	local deltaLevel = 0;
	
	local x, y, z = getWorldTranslation(self.manureTrigger);
	local dx, dy, dz = localDirectionToWorld(self.manureTrigger, 0, -1, 0);
	raycastAll(x, y, z, dx, dy, dz, "findManureTriggerRaycastCallback", 1.7, self);	
	
	if self.controllingPlayer ~= nil then	
		-- standard tip and alternative tipping trigger support --
		if self.fillLevel > 0 then
			for k, tipTrigger in pairs(g_currentMission.tipTriggers) do
				local trailerX, trailerY, trailerZ = getWorldTranslation(self.tipReferencePoint);
				local triggerX, triggerY, triggerZ = getWorldTranslation(tipTrigger.triggerId);
				local distance = Utils.vector3Length(trailerX-triggerX, trailerY-triggerY, trailerZ-triggerZ);
				if distance < 3 then
					self.unloadingTipTrigger = tipTrigger;
				end;
			end;
		end;

		if self.unloadingTipTrigger ~= nil then
			local tipTrigger = self.unloadingTipTrigger;
			local fruitType = FruitUtil.fillTypeToFruitType[self.currentFillType];
			if tipTrigger.currentFillType ~= nil then
				fruitAccept = (tipTrigger.currentFillType == self.currentFillType);
			else
				fruitAccept = tipTrigger.acceptedFruitTypes[fruitType];
			end;
			if tipTrigger.isFarmTrigger and fruitAccept then
				if self.isPitchforkDischarge and self.animationParts[3].clipEndTime then
					deltaLevel = self.unloadingCapacity*dt/500;
					if g_currentMission.missionStats.farmSiloAmounts[self.currentFillType] ~= nil then
						g_currentMission.missionStats.farmSiloAmounts[self.currentFillType] = g_currentMission.missionStats.farmSiloAmounts[self.currentFillType] + deltaLevel;
					end;
				else
					self.renderUnloadOverlay = true;
				end;
			elseif fruitAccept then
				if self.isPitchforkDischarge and self.animationParts[3].clipEndTime then
					deltaLevel = self.unloadingCapacity*dt/500;
					if self.isServer then
						local priceMultiplier = tipTrigger.priceMultipliers[fruitType];
						local difficultyMultiplier = math.max(3 * (3 - g_currentMission.missionStats.difficulty), 1);
						local money = FruitUtil.fruitIndexToDesc[fruitType].pricePerLiter * priceMultiplier * difficultyMultiplier * deltaLevel;
						g_currentMission:addSharedMoney(money);
					end;
				else
					self.renderUnloadOverlay = true;
				end;
			else
				if fruitType ~= nil then
					if FruitUtil.fruitIndexToDesc[fruitType].name ~= nil then
						self.isPitchforkDischarge = false;
						g_currentMission:addWarning(g_i18n:getText(FruitUtil.fruitIndexToDesc[fruitType].name) .. g_i18n:getText("notAcceptedHere"), 0.018, 0.033);
					end;
				end;
			end;
			if deltaLevel > 0 then
				if self.currentFillType == Fillable.FILLTYPE_HAYBALE or self.currentFillType == Fillable.FILLTYPE_STRAWBALE then
					deltaLevel = self.fillLevel;
					if self.mountedBaleId ~= nil then
						if self.isServer then
							local bale = networkGetObject(self.mountedBaleId);
							self:attachBale(nil);
							if bale ~= nil then
								bale:delete();
							end;
						end;
					end;
				end;
				tipTrigger:updateMoving(deltaLevel);
			end;
			self:setFillLevel(self.fillLevel - deltaLevel, self.currentFillType);
		
		-- standard manure trigger --
		elseif self.manureTriggerFound ~= 0 then
			if self.fillLevel < self.capacity then
				if not self.fallOut then
					deltaLevel = self.unloadingCapacity*dt/100;
					if g_currentMission.missionStats.farmSiloAmounts[Fillable.FILLTYPE_MANURE] > 0 then
						self:setFillLevel(self.fillLevel + deltaLevel, Fillable.FILLTYPE_MANURE);
					end;
					g_currentMission.missionStats.farmSiloAmounts[Fillable.FILLTYPE_MANURE] = g_currentMission.missionStats.farmSiloAmounts[Fillable.FILLTYPE_MANURE] - deltaLevel;
					if g_currentMission.missionStats.farmSiloAmounts[Fillable.FILLTYPE_MANURE] < 0 then
						g_currentMission.missionStats.farmSiloAmounts[Fillable.FILLTYPE_MANURE] = 0;
					end;
				end;
			end;		
		end;
	
		-- trailer load support --
		local nearestDistance = 4;
		local px, py, pz = getWorldTranslation(self.trailerTrigger);
		for i=1, table.getn(g_currentMission.attachables) do
			if g_currentMission.attachables[i] ~= self then
				local vx, vy, vz = getWorldTranslation(g_currentMission.attachables[i].rootNode);
				local distance = Utils.vector3Length(px-vx, py-vy, pz-vz);
				if distance < nearestDistance then
					self.trailerFoundId = g_currentMission.attachables[i];
					local deltaLevel = 0;
					if self.trailerFoundId ~= nil and self.trailerFoundId ~= 0 then
						for loadToTrailerFillType,v in pairs(self.loadToTrailerFillTypes) do
							if loadToTrailerFillType == self.currentFillType then	
								local trailer = self.trailerFoundId;
								if trailer ~= nil and trailer ~= self then
									if self.fillLevel > 0 and trailer.allowFillType ~= nil then
										local trailerAllowFillType = false;
										if self.currentFillType == Fillable.FILLTYPE_HAYBALE or self.currentFillType == Fillable.FILLTYPE_STRAWBALE then
											trailerAllowFillType = trailer:allowFillType(Fillable.FILLTYPE_HAYBALE, false) or trailer:allowFillType(Fillable.FILLTYPE_STRAWBALE, false);
										else
											trailerAllowFillType = trailer:allowFillType(self.currentFillType, false);
										end;
										if trailerAllowFillType then
											if trailer.allowFillFromAir then
												if trailer.capacity > trailer.fillLevel and self.fillLevel > 0 then
													self.canLoadToTrailer = true;
													if self.isPitchforkDischarge then
														if self.animationParts[4].clipEndTime then
															if self.currentFillType ~= Fillable.FILLTYPE_HAYBALE and self.currentFillType ~= Fillable.FILLTYPE_STRAWBALE then
																local deltaLevel = self.unloadingCapacity*dt/100;
																local fillDelta = math.min(deltaLevel, self.fillLevel);
																self:setFillLevel(self.fillLevel - fillDelta, self.currentFillType);
																trailer:setFillLevel(trailer.fillLevel + fillDelta, self.currentFillType);
															elseif self.mountedBaleId ~= nil then
																if self.isServer then
																	local bale = networkGetObject(self.mountedBaleId);
																	self:attachBale(nil);
																	if bale ~= nil then
																		bale:delete();
																	end;
																end;
																if trailer.currentFillType == Fillable.FILLTYPE_UNKNOWN then
																	filltype = self.currentFillType;
																else
																	filltype = trailer.currentFillType;
																end;
																trailer:setFillLevel(trailer.fillLevel + trailer.baleFillValue, filltype);
																self:setFillLevel(0, Fillable.FILLTYPE_UNKNOWN, true);
															end;
														end;
													end;
												end;
											end;
										end;
									end;
								end;
							end;
						end;
					end;
				end;
			end;
		end;
		
		if not self.controllingPlayer.isControlled and not self.controllingPlayer.isEntered then
			self:setActiveState(nil);
		end;
		
		if self.controllingPlayer ~= nil then
			local x,y,z = getWorldTranslation(self.controllingPlayer.rootNode);
			self.pitchfork.rootNodeTrans = {x,y,z};
		end;
	end;

	-- fruit collecting --
	if self.isPitchforkActive and self.fillLevel < self.capacity then		
		if self.animationParts[2].clipEndTime then
			-- bale support --
			if self.currentFillType == Fillable.FILLTYPE_UNKNOWN and self.fillLevel == 0 then
				local nearestDistance = 1;
				local nearestBale = nil;
				local px, py, pz = getWorldTranslation(self.baleTrigger);
				for index, item in pairs(g_currentMission.itemsToSave) do
					itemNode = item.item.nodeId;
					if getParent(item.item.nodeId) == getRootNode() then
						if getUserAttribute(itemNode, "isStrawbale") or getUserAttribute(itemNode, "isHaybale") then
							local vx, vy, vz = getWorldTranslation(itemNode);
							local distance = Utils.vector3Length(px-vx, py-vy, pz-vz);
							if distance < nearestDistance then
								index = index;
								nearestBale = item.item;
								nearestObject = itemNode;
								nearestDistance = distance;
							end;
						end;
					end;
				end;
				
				if nearestBale ~= nil then
					baleId = networkGetObjectId(nearestBale);
					nearestBale = networkGetObject(baleId);
					if getUserAttribute(nearestBale.nodeId, "isStrawbale") then
						fillType = Fillable.FILLTYPE_STRAWBALE;
					elseif getUserAttribute(nearestBale.nodeId, "isHaybale") then
						fillType = Fillable.FILLTYPE_HAYBALE;
					end;
					if fillType ~= nil then
						if self.isServer then
							self:attachBale(baleId);
						end;
						if self.mountedBaleId ~= nil then
							self:setFillLevel(self.capacity, fillType);
						end;
					end;
				end;
			end;
			
			-- fruit support --			
			local area = 0;
			for k, cuttingArea in pairs(self.cuttingAreas) do
				local x,y,z = getWorldTranslation(cuttingArea.start);
				local x1,y1,z1 = getWorldTranslation(cuttingArea.width);
				local x2,y2,z2 = getWorldTranslation(cuttingArea.height);
				for fillType,v in pairs(self.fillTypes) do
					if fillType ~= Fillable.FILLTYPE_UNKNOWN then
						if self.currentFillType == fillType or self.currentFillType == Fillable.FILLTYPE_UNKNOWN then
							local fruitType = FruitUtil.fillTypeToFruitType[fillType];
							-- switch manure to manurev2
							if fruitType == FruitUtil.FRUITTYPE_MANURE then
								fruitType = FruitUtil.FRUITTYPE_MANURE2;
							end;
							local area = Utils.updateFruitCutLongArea(fruitType, x, z, x1, z1, x2, z2, 0);
							area = area + Utils.updateFruitWindrowArea(fruitType, x, z, x1, z1, x2, z2, 0);
							if fruitType == FruitUtil.FRUITTYPE_GRASS then
								area = area + Utils.updateFruitCutLongArea(FruitUtil.FRUITTYPE_DRYGRASS, x, z, x1, z1, x2, z2, 0);
								area = area + Utils.updateFruitWindrowArea(FruitUtil.FRUITTYPE_DRYGRASS, x, z, x1, z1, x2, z2, 0);
							end;
							if area > 0 then
								--if self.currentFruitType == FruitUtil.FRUITTYPE_GRASS or self.currentFruitType == FruitUtil.FRUITTYPE_DRYGRASS then
									literPerSqm = g_strawLitersPerSqm*2;
								--else
									--literPerSqm = FruitUtil.fruitIndexToDesc[self.currentFruitType].literPerSqm; --* (1 + 0.50 * (3 - g_currentMission.missionStats.difficulty));
								--end;
								local pixelToSqm = g_currentMission:getFruitPixelsToSqm();
								local sqm = area*pixelToSqm;
								local deltaLevel = sqm*literPerSqm*self.fillScale;
								self:setFillLevel(self.fillLevel+deltaLevel, fillType);
							end;
						end;
					end;
				end;
			end;
		end;
		
	-- fruit discharge --
	elseif self.isPitchforkDischarge and self.fillLevel > 0 and self.canLoadToTrailer == false and self.unloadingTipTrigger == nil then
		if self.animationParts[3].clipEndTime then
			if self.currentFillType ~= Fillable.FILLTYPE_HAYBALE and self.currentFillType ~= Fillable.FILLTYPE_STRAWBALE then
				local area = 0;
				for k, cuttingArea in pairs(self.cuttingAreas) do
					local x,y,z = getWorldTranslation(cuttingArea.start);
					local x1,y1,z1 = getWorldTranslation(cuttingArea.width);
					local x2,y2,z2 = getWorldTranslation(cuttingArea.height);
					local fruitType = FruitUtil.fillTypeToFruitType[self.currentFillType];
					-- switch manure to manurev2
					if self.currentFillType == Fillable.FILLTYPE_MANURE then
						fruitType = FruitUtil.FRUITTYPE_MANURE2;
					elseif self.currentFillType ==  Fillable.FILLTYPE_GRASS then
						fruitType = FruitUtil.FRUITTYPE_DRYGRASS;
					end;
					local deltaLevel = self.unloadingCapacity*dt/1000;
					local fillDelta = math.min(deltaLevel, self.fillLevel);
					local area = Utils.updateFruitWindrowArea(fruitType, x, z, x1, z1, x2, z2, 1, true, false);
					self:setFillLevel(self.fillLevel - fillDelta, self.currentFillType);
				end;
			elseif self.mountedBaleId ~= nil then
				self:setFillLevel(0, Fillable.FILLTYPE_UNKNOWN, true);
				if self.isServer then
					self:attachBale(nil);
				end;
			end;
		end;
	end;
	if self.fillLevel >= self.capacity then
		self.isPitchforkActive = false;
	elseif self.fillLevel <= 0 then
		self.isPitchforkDischarge = false;
		self.currentFillType = Fillable.FILLTYPE_UNKNOWN;
	end;
	
	local tempMass = getMass(self.rootNode);
	if self.isSticked then
		if tempMass <= 4.9 or tempMass >= 5.1 then
			setMass(self.rootNode, 5);
			setCenterOfMass(self.rootNode, 0, -3, 0);
		end;
	else
		if tempMass <= 0.004 or tempMass >= 0.006 then
			setMass(self.rootNode, 0.005);
			setCenterOfMass(self.rootNode, 0, 0.8, -0.1);
		end;
	end;
	
	if self.currentFillType ~= Fillable.FILLTYPE_UNKNOWN then
		Utils.setEmittingState(self.unloadingParticleSystems[self.currentFillType], self.isPitchforkDischarge);
	else
		for k, ps in pairs(self.unloadingParticleSystems) do
			Utils.setEmittingState(self.unloadingParticleSystems[k], false);
		end;
	end;
	
	if self.isPitchforkActive then
		self:setAnimationTime(2, self.animationParts[2].animDuration, true);
	else
		self:setAnimationTime(2, self.animationParts[2].startPosition, true);
	end;
	
	if self.isPitchforkDischarge then
		if self.canLoadToTrailer == false then
			self:setAnimationTime(3, self.animationParts[3].animDuration, true);
			self:setAnimationTime(4, self.animationParts[4].startPosition, true);
		else
			self:setAnimationTime(4, self.animationParts[4].animDuration, true);
			self:setAnimationTime(3, self.animationParts[3].startPosition, true);
		end;
	else
		self:setAnimationTime(3, self.animationParts[3].startPosition, true);
		self:setAnimationTime(4, self.animationParts[4].startPosition, true);
	end;
	
	-- detach pitchfork from vehicle --
	if self.attacherVehicle ~= nil then
		for i=1, table.getn(self.attacherVehicle.attachedImplements) do
			if self.attacherVehicle.attachedImplements[i].object == self then
				local implement = self.attacherVehicle.attachedImplements[i];
				local jointDesc = self.attacherVehicle.attacherJoints[implement.jointDescIndex];
				for i=1, 3 do
					setJointRotationLimit(jointDesc.jointIndex, i-1, true, Utils.degToRad(0), Utils.degToRad(0));
				end;
			end;
		end;
	end;
	
	if self.isPitchforkActive and self.animationParts[2].clipEndTime then
		self.fallOut = false;
	else
		self.fallOut = true;
	end;
end;

function Pitchfork:updateTick(dt)
	if self.controllingPlayer == nil then
		if g_currentMission.currentVehicle == nil then
			self.isPlayerInRange = Pitchfork.getPlayerInRange(self, g_currentMission.player);
		end;
	end;
end;

function Pitchfork:draw()
	if self.isEntered then
		self.hudBackgroundOverlay:render();

		local kmh = math.min(100, math.max(0, self.lastSpeed*self.speedDisplayScale*3600));
		
		setTextBold(true);

		setTextColor(1.0, 1.0, 1.0, 1.0);
		setTextAlignment(RenderText.ALIGN_CENTER);

		local maxSpeed = 80;

		setTextColor(0, 0, 0, 1);
		renderText(self.hudBasePosX + self.hudBaseWidth / 2 + 0.002, self.hudBasePosY + 0.005 + 3 * 0.0396, 0.024, string.format("%2d " .. g_i18n:getText("speedometer"), kmh));
		setTextColor(1, 1, 1, 1);
		renderText(self.hudBasePosX + self.hudBaseWidth / 2 + 0.002, self.hudBasePosY + 0.008 + 3 * 0.0396, 0.024, string.format("%2d " .. g_i18n:getText("speedometer"), kmh));

		self.hudBarRedOverlay.width = self.hudBarWidth * math.min(1, kmh / maxSpeed);
		setOverlayUVs(self.hudBarRedOverlay.overlayId, 0, 0.05, 0, 1, math.min(1, kmh / maxSpeed), 0.05, math.min(1, kmh / maxSpeed), 1);
		self.hudBarRedOverlay:render();

		setTextColor(0, 0, 0, 1);
		renderText(self.hudBasePosX + self.hudBaseWidth / 2 + 0.002, self.hudBasePosY + 0.005 + 2 * 0.0396, 0.024, string.format("%1.0f " .. g_i18n:getText("Currency_symbol"), g_currentMission.missionStats.money));
		setTextColor(1, 1, 1, 1);
		renderText(self.hudBasePosX + self.hudBaseWidth / 2 + 0.002, self.hudBasePosY + 0.008 + 2 * 0.0396, 0.024, string.format("%1.0f " .. g_i18n:getText("Currency_symbol"), g_currentMission.missionStats.money));

		local currentFuelPercentage = 0;
		local fuelWarnPercentage = 0;
	
		self:drawGrainLevel(self.fillLevel, self.capacity, 101);

		setTextAlignment(RenderText.ALIGN_LEFT);
		setTextBold(false);	
		
		if self.currentFillType ~= Fillable.FILLTYPE_UNKNOWN then
			local fruitType = FruitUtil.fillTypeToFruitType[self.currentFillType];
			if fruitType ~= nil then
				g_currentMission:setFruitOverlayFruitType(fruitType);
			end;
		end;
		if self.renderUnloadOverlay then
			self.unloadingOverlay:render();
		end;
	end;
end;

function Pitchfork:stickPitchfork(stickState, noEventSend)
	PitchforkStickEvent.sendEvent(self, stickState, noEventSend);
	self.isSticked = stickState;
end;

function Pitchfork:attachBale(baleId, noEventSend)
	BaleHandlerEvent.sendEvent(self, baleId, noEventSend);
	if baleId ~= nil then
		if self.baleTrigger ~= nil then
			local bale = nil;

			if self.mountedBaleId ~= nil then
				bale = networkGetObject(self.mountedBaleId);
				if bale ~= nil then
					bale:unmount();
					self.mountedBaleId = nil;
				end;
			end;
			bale = networkGetObject(baleId);
			if bale ~= nil then
				bale:mount(self, self.baleTrigger, 0,0,0, 0,0,0);
				self.mountedBaleId = baleId;
			end;
		end;
	else
		if self.mountedBaleId ~= nil then
			local bale = networkGetObject(self.mountedBaleId);
			if bale ~= nil then
				bale:unmount();
				self.mountedBaleId = nil;
			end;
		end;
	end;
end;

function Pitchfork:setActiveState(player, noEventSend)	
	PitchforkEnableEvent.sendEvent(self, player, noEventSend);
	self.controllingPlayer = player;	
	if player ~= nil then
		if self.controllingPlayer == g_currentMission.player then
			self.isEntered = true;
			setTranslation(self.pitchfork.node, 0,1.25,0.03);
		else
			self.isEntered = false;
			setTranslation(self.pitchfork.node, -0.18,0.91,0.57);
		end;
		self:setAnimationTime(1, self.animationParts[1].animDuration, true);
	else
		self.isEntered = false;
		if self.isPitchforkActive then
			self.isPitchforkActive = false;
		end;
		if self.isPitchforkDischarge then
			self.isPitchforkDischarge = false;
		end;
		self:setAnimationTime(1, self.animationParts[1].startPosition, true);
		setTranslation(self.pitchfork.rootNode, unpack(self.pitchfork.rootNodeTrans));
		setTranslation(self.pitchfork.node, 0,1.03,0.03);
	end;
	self.hasPlayerQuit = false;
end;

function Pitchfork:setPitchforkActive(isActive, noEventSend)
	PitchforkActiveEvent.sendEvent(self, isActive, noEventSend);
	self.isPitchforkActive = isActive;
end;

function Pitchfork:setPitchforkDischarge(isActive, noEventSend)
	PitchforkDischargeEvent.sendEvent(self, isActive, noEventSend);
	self.isPitchforkDischarge = isActive;
end;

function Pitchfork:getPlayerInRange(player)
	local isPlayerInRange = false; 
	if player ~= nil then
		local px, py, pz = getWorldTranslation(self.rootNode);
		local vx, vy, vz = getWorldTranslation(player.rootNode);
		local distance = Utils.vector2Length(px-vx, pz-vz);
		if distance < self.pitchfork.actionDistance then		
			isPlayerInRange = true;
		end; 
	end;
	
	return isPlayerInRange;
end;

function Pitchfork:onAttach(attacherVehicle)
	self:setActiveState(nil);
end;

function Pitchfork:drawGrainLevel(level, capacity, warnPercent)
    local percent = 0;
    if capacity > 0 then -- skip if there's no capacity
        percent = level / capacity * 100;

        setTextBold(true);
        setTextAlignment(RenderText.ALIGN_CENTER);

        setTextColor(0, 0, 0, 1);
        renderText(self.hudBasePosX + self.hudBaseWidth / 2 + 0.002, self.hudBasePosY + 0.005 + 0.0396, 0.024, string.format("%d (%d%%)", level, percent));

        if percent >= warnPercent then
            setTextColor(1, 1, 1, 1);
        else
            setTextColor(1, 1, 1, 1);
        end;
        renderText(self.hudBasePosX + self.hudBaseWidth / 2 + 0.002, self.hudBasePosY + 0.008 + 0.0396, 0.024, string.format("%d (%d%%)", level, percent));
        if percent >= warnPercent then
            setTextColor(1, 1, 1, 1);
        end;
        setTextAlignment(RenderText.ALIGN_LEFT);
        setTextBold(false);
        -- render the grain fill level bar here (as this function is also called by the combine)
        self.hudBarGoldOverlay.width = self.hudBarWidth * (level / capacity); -- TODO: check for division by zero!
        setOverlayUVs(self.hudBarGoldOverlay.overlayId, 0, 0.05, 0, 1, level / capacity, 0.05, level / capacity, 1);
        self.hudBarGoldOverlay:render();
    end;

    -- render HUD frame overlay last (after all bars have been rendered)
    self.hudFramesOverlay:render();
end;

function Pitchfork:getSaveAttributesAndNodes(nodeIdent)
	if self.mountedBaleId ~= nil then
		if self.isServer then
			self:attachBale(nil);
		end;
		self:setFillLevel(0, Fillable.FILLTYPE_UNKNOWN, true);
	end;
	local attributes = 'isSticked="'.. tostring(self.isSticked) ..'"';
	return attributes, nil;
end;

function Pitchfork:loadFromAttributesAndNodes(xmlFile, key, resetVehicles)
	if not resetVehicles then
		local stickState = Utils.getNoNil(getXMLBool(xmlFile, key.."#isSticked"), false);
		if stickState == true then
			self:stickPitchfork(stickState);
		end;
	end;
	return BaseMission.VEHICLE_LOAD_OK;
end;

function Pitchfork:findManureTriggerRaycastCallback(transformId, x, y, z, distance)
	local id = 0;
    if getCollisionMask(transformId) == 8192 then
        id = transformId;
    end;
	self.manureTriggerFound = id;
	return;
end;

--------------
--- events ---
--------------

PitchforkEnableEvent = {};
PitchforkEnableEvent_mt = Class(PitchforkEnableEvent, Event);

InitEventClass(PitchforkEnableEvent, "PitchforkEnableEvent");

function PitchforkEnableEvent:emptyNew()
    local self = Event:new(PitchforkEnableEvent_mt);
    self.className="PitchforkEnableEvent";
    return self;
end;

function PitchforkEnableEvent:new(vehicle, player)
    local self = PitchforkEnableEvent:emptyNew()
    self.vehicle = vehicle;
	self.player = player;
    return self;
end;

function PitchforkEnableEvent:readStream(streamId, connection)
    local id = streamReadInt32(streamId);
	local playerId = streamReadInt32(streamId);
	self.player = networkGetObject(playerId);
    self.vehicle = networkGetObject(id);
    self:run(connection);
end;

function PitchforkEnableEvent:writeStream(streamId, connection)
    streamWriteInt32(streamId, networkGetObjectId(self.vehicle));
	streamWriteInt32(streamId, networkGetObjectId(self.player));	
end;

function PitchforkEnableEvent:run(connection)
	self.vehicle:setActiveState(self.player, true);
	if not connection:getIsServer() then
		g_server:broadcastEvent(PitchforkEnableEvent:new(self.vehicle, self.player), nil, connection, self.object);
	end;
end;

function PitchforkEnableEvent.sendEvent(vehicle, player, noEventSend)
	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(PitchforkEnableEvent:new(vehicle, player), nil, nil, vehicle);
		else
			g_client:getServerConnection():sendEvent(PitchforkEnableEvent:new(vehicle, player));
		end;
	end;
end;


PitchforkActiveEvent = {};
PitchforkActiveEvent_mt = Class(PitchforkActiveEvent, Event);

InitEventClass(PitchforkActiveEvent, "PitchforkActiveEvent");

function PitchforkActiveEvent:emptyNew()
    local self = Event:new(PitchforkActiveEvent_mt);
    self.className="PitchforkActiveEvent";
    return self;
end;

function PitchforkActiveEvent:new(vehicle, activeState)
    local self = PitchforkActiveEvent:emptyNew()
    self.vehicle = vehicle;
	self.activeState = activeState;
    return self;
end;

function PitchforkActiveEvent:readStream(streamId, connection)
    local id = streamReadInt32(streamId);
	self.activeState = streamReadBool(streamId);
    self.vehicle = networkGetObject(id);
    self:run(connection);
end;

function PitchforkActiveEvent:writeStream(streamId, connection)
    streamWriteInt32(streamId, networkGetObjectId(self.vehicle));
	streamWriteBool(streamId, self.activeState);
end;

function PitchforkActiveEvent:run(connection)   
	self.vehicle:setPitchforkActive(self.activeState, true);
    if not connection:getIsServer() then
        g_server:broadcastEvent(PitchforkActiveEvent:new(self.vehicle, self.activeState), nil, connection, self.vehicle);
    end;
end;

function PitchforkActiveEvent.sendEvent(vehicle, activeState, noEventSend)
	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(PitchforkActiveEvent:new(vehicle, activeState), nil, nil, vehicle);
		else
			g_client:getServerConnection():sendEvent(PitchforkActiveEvent:new(vehicle, activeState));
		end;
	end;
end;


PitchforkDischargeEvent = {};
PitchforkDischargeEvent_mt = Class(PitchforkDischargeEvent, Event);

InitEventClass(PitchforkDischargeEvent, "PitchforkDischargeEvent");

function PitchforkDischargeEvent:emptyNew()
    local self = Event:new(PitchforkDischargeEvent_mt);
    self.className="PitchforkDischargeEvent";
    return self;
end;

function PitchforkDischargeEvent:new(vehicle, dischargeState)
    local self = PitchforkDischargeEvent:emptyNew()
    self.vehicle = vehicle;
	self.dischargeState = dischargeState;
    return self;
end;

function PitchforkDischargeEvent:readStream(streamId, connection)
    local id = streamReadInt32(streamId);
	self.dischargeState = streamReadBool(streamId);
    self.vehicle = networkGetObject(id);
    self:run(connection);
end;

function PitchforkDischargeEvent:writeStream(streamId, connection)
    streamWriteInt32(streamId, networkGetObjectId(self.vehicle));
	streamWriteBool(streamId, self.dischargeState);
end;

function PitchforkDischargeEvent:run(connection)   
	self.vehicle:setPitchforkDischarge(self.dischargeState, true);
    if not connection:getIsServer() then
        g_server:broadcastEvent(PitchforkDischargeEvent:new(self.vehicle, self.dischargeState), nil, connection, self.vehicle);
    end;
end;

function PitchforkDischargeEvent.sendEvent(vehicle, dischargeState, noEventSend)
	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(PitchforkDischargeEvent:new(vehicle, dischargeState), nil, nil, vehicle);
		else
			g_client:getServerConnection():sendEvent(PitchforkDischargeEvent:new(vehicle, dischargeState));
		end;
	end;
end;


PitchforkStickEvent = {};
PitchforkStickEvent_mt = Class(PitchforkStickEvent, Event);

InitEventClass(PitchforkStickEvent, "PitchforkStickEvent");

function PitchforkStickEvent:emptyNew()
    local self = Event:new(PitchforkStickEvent_mt);
    self.className="PitchforkStickEvent";
    return self;
end;

function PitchforkStickEvent:new(vehicle, stickState)
    local self = PitchforkStickEvent:emptyNew()
    self.vehicle = vehicle;
	self.stickState = stickState;
    return self;
end;

function PitchforkStickEvent:readStream(streamId, connection)
    local id = streamReadInt32(streamId);
	self.stickState = streamReadBool(streamId);
    self.vehicle = networkGetObject(id);
    self:run(connection);
end;

function PitchforkStickEvent:writeStream(streamId, connection)
    streamWriteInt32(streamId, networkGetObjectId(self.vehicle));
	streamWriteBool(streamId, self.stickState);
end;

function PitchforkStickEvent:run(connection)   
	self.vehicle:stickPitchfork(self.stickState, true);
    if not connection:getIsServer() then
        g_server:broadcastEvent(PitchforkStickEvent:new(self.vehicle, self.stickState), nil, connection, self.vehicle);
    end;
end;

function PitchforkStickEvent.sendEvent(vehicle, stickState, noEventSend)
	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(PitchforkStickEvent:new(vehicle, stickState), nil, nil, vehicle);
		else
			g_client:getServerConnection():sendEvent(PitchforkStickEvent:new(vehicle, stickState));
		end;
	end;
end;

BaleHandlerEvent = {};
BaleHandlerEvent_mt = Class(BaleHandlerEvent, Event);

InitEventClass(BaleHandlerEvent, "BaleHandlerEvent");

function BaleHandlerEvent:emptyNew()
    local self = Event:new(BaleHandlerEvent_mt);
    self.className="BaleHandlerEvent";
    return self;
end;

function BaleHandlerEvent:new(vehicle, baleId)
    local self = BaleHandlerEvent:emptyNew()
    self.vehicle = vehicle;
	self.baleId = baleId;
    return self;
end;

function BaleHandlerEvent:readStream(streamId, connection)
    local id = streamReadInt32(streamId);
    self.baleId = streamReadInt32(streamId);
    self.vehicle = networkGetObject(id);
    self:run(connection);
end;

function BaleHandlerEvent:writeStream(streamId, connection)
    streamWriteInt32(streamId, networkGetObjectId(self.vehicle));
	streamWriteInt32(streamId, self.baleId);
end;

function BaleHandlerEvent:run(connection)
	self.vehicle:attachBale(self.baleId, true);
	if not connection:getIsServer() then
		g_server:broadcastEvent(BaleHandlerEvent:new(self.vehicle, self.baleId), nil, connection, self.object);
	end;
end;

function BaleHandlerEvent.sendEvent(vehicle, baleId, noEventSend)
	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(BaleHandlerEvent:new(vehicle, baleId), nil, nil, vehicle);
		else
			g_client:getServerConnection():sendEvent(BaleHandlerEvent:new(vehicle, baleId));
		end;
	end;
end;