--
-- EasyFlow
-- This is the specialization for EasyFlow
-- It reacts both to steerable and attachable events
--
-- @author  Stefan Geiger
-- @date  04/12/08
--
--
-- Copyright (C) GIANTS Software GmbH, Confidential, All Rights Reserved.
--
-- @version 1.0
-- @coAuthor Milan Lekše [milan1702976]
-- @date  18/02/13
--	
-- @version 1.1
-- @coAuthor Milan Lekše [milan1702976]
-- @date  18/03/13
-- @changelog
--
-- grass output grass or chaff by choice
-- new improved swath search
-- follow ground control
-- particle system
--
--
-- Copyright (C) www.FS-UK.com, Confidential, All Rights Reserved.

EasyFlow = {};

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

function EasyFlow:load(xmlFile)

    self.onStartReel = SpecializationUtil.callSpecializationsFunction("onStartReel");
    self.onStopReel = SpecializationUtil.callSpecializationsFunction("onStopReel");
    self.combineAIupdateGrass = SpecializationUtil.callSpecializationsFunction("combineAIupdateGrass");
    self.combineAIupdateChaff = SpecializationUtil.callSpecializationsFunction("combineAIupdateChaff");
    self.combineAIupdateTickGrass = SpecializationUtil.callSpecializationsFunction("combineAIupdateTickGrass");
    self.combineAIupdateTickChaff = SpecializationUtil.callSpecializationsFunction("combineAIupdateTickChaff");
    self.changeGrassForceGrass = EasyFlow.changeGrassForceGrass;

    --self.setFruitType = SpecializationUtil.callSpecializationsFunction("setFruitType");
    self.getCombine = EasyFlow.getCombine;
    self.applyInitialAnimation = Utils.overwrittenFunction(self.applyInitialAnimation, EasyFlow.applyInitialAnimation);
    self.getDirectionSnapAngle = Utils.overwrittenFunction(self.getDirectionSnapAngle, EasyFlow.getDirectionSnapAngle);

    self.reelNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.reel#index"));
    self.reelSpeed = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.reel#speed"), 0.003);

    self.reelSpeedScale = 1;

    self.rollNodes = {};

    local rollNode = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.roll#index"));
    if rollNode ~= nil then
        local speed = 0.003*3;
        table.insert(self.rollNodes, {node=rollNode, speed=speed});
    end;
    local i = 0;
    while true do
        local key = string.format("vehicle.rolls.roll(%d)", i);
        if not hasXMLProperty(xmlFile, key) then
            break;
        end;
        local rollNode = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#index"));
        local speed = Utils.getNoNil(getXMLFloat(xmlFile, key.."#speed"), 0.003);
        if rollNode ~= nil then
            table.insert(self.rollNodes, {node=rollNode, speed=speed});
        end;
        i = i + 1;
    end;

    local indexSpikesStr = getXMLString(xmlFile, "vehicle.reelspikes#index");
    self.spikesCount = getXMLInt(xmlFile, "vehicle.reelspikes#count");
    self.spikesRootNode = Utils.indexToObject(self.components, indexSpikesStr);

    self.sideArm = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.sidearms#index"));
    self.sideArmMovable = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.sidearms#movable"), false);

    self.cutterStartAnimation = getXMLString(xmlFile, "vehicle.cutterStartAnimation#name");
    self.cutterStartAnimationSpeedScale = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.cutterStartAnimation#speedScale"), 1);
    self.cutterStartAnimationInitialIsStarted = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.cutterStartAnimation#initialIsStarted"), false);

    self.cutterAllowCuttingWhileRaised = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.cutterAllowCuttingWhileRaised"), false);

    self.cutterSpeedRotatingParts = {};
    local i=0;
    while true do
        local baseName = string.format("vehicle.cutterSpeedRotatingParts.cutterSpeedRotatingPart(%d)", i);
        local index = getXMLString(xmlFile, baseName.. "#index");
        if index == nil then
            break;
        end;
        local node = Utils.indexToObject(self.components, index);
        if node ~= nil then
            local entry = {};
            entry.node = node;
            entry.rotationSpeedScale = getXMLFloat(xmlFile, baseName.."#rotationSpeedScale");
            if entry.rotationSpeedScale == nil then
                entry.rotationSpeedScale = 1.0/Utils.getNoNil(getXMLFloat(xmlFile, baseName.."#radius"), 1);
            end;

            entry.foldMinLimit = Utils.getNoNil(getXMLFloat(xmlFile, baseName .. "#foldMinLimit"), 0);
            entry.foldMaxLimit = Utils.getNoNil(getXMLFloat(xmlFile, baseName .. "#foldMaxLimit"), 1);

            table.insert(self.cutterSpeedRotatingParts, entry);
        end;
        i = i+1;
    end;

    self.threshingParticleSystems = {};
    local psName = "vehicle.threshingParticleSystem";
    Utils.loadParticleSystem(xmlFile, self.threshingParticleSystems, psName, self.components, false, nil, self.baseDirectory)

    self.fruitExtraObjects = {};
    local i = 0;
    while true do
        local key = string.format("vehicle.fruitExtraObjects.fruitExtraObject(%d)", i);
        local t = getXMLString(xmlFile, key.."#fruitType");
        local index = getXMLString(xmlFile, key.."#index");
        if t==nil or index==nil then
            break;
        end;

        local node = Utils.indexToObject(self.components, index);
        if node ~= nil then
            if self.currentExtraObject == nil then
                self.currentExtraObject = node;
                setVisibility(node, true);
            else
                setVisibility(node, false);
            end;
            self.fruitExtraObjects[t] = node;
        end;
        i = i +1;
    end;

    self.cutterThreshingUVScrollParts = {};
    local i = 0;
    while true do
        local key = string.format("vehicle.cutterThreshingUVScrollParts.cutterThreshingUVScrollPart(%d)", i);
        if not hasXMLProperty(xmlFile, key) then
            break;
        end;
        local node = Utils.indexToObject(self.components, getXMLString(xmlFile, key.."#index"));
        local speed = Utils.getVectorNFromString(getXMLString(xmlFile, key.."#speed"), 2);
        if node ~= nil and speed then
            table.insert(self.cutterThreshingUVScrollParts, {node=node, speed=speed});
        end;
        i = i +1;
    end;

    self.preferedCombineSize = Utils.getNoNil(getXMLInt(xmlFile, "vehicle.preferedCombineSize"), 1);

    self.fruitTypes = {};
    local fruitTypes = getXMLString(xmlFile, "vehicle.fruitTypes#fruitTypes");
    if fruitTypes ~= nil then
        local types = Utils.splitString(" ", fruitTypes);
        for k,v in pairs(types) do
            local desc = FruitUtil.fruitTypes[v];
            if desc ~= nil then
                self.fruitTypes[desc.index] = true;
            end;
        end;
    end;

    self.convertedFruits = {};
    local i = 0;
    while true do
        local key = string.format("vehicle.convertedFruits.convertedFruit(%d)", i);
        if not hasXMLProperty(xmlFile, key) then
            break;
        end;
        local inputType = getXMLString(xmlFile, key .. "#input");
        local outputType = getXMLString(xmlFile, key .. "#output");

        if inputType ~= nil and outputType ~= nil then
            local inputDesc = FruitUtil.fruitTypes[inputType];
            local outputDesc = FruitUtil.fruitTypes[outputType];
            if inputDesc ~= nil and outputDesc ~= nil then
                self.convertedFruits[inputDesc.index] = outputDesc.index;
            end;
        end;

        i = i + 1;
    end;

    self.cutterMovingDirection = Utils.sign(Utils.getNoNil(getXMLInt(xmlFile, "vehicle.cutterMovingDirection"), -1));

    self.currentInputFruitType = FruitUtil.FRUITTYPE_UNKNOWN;

    self.reelStarted = false;

    self.forceLowSpeed = Utils.getNoNil(getXMLBool(xmlFile, "vehicle.cutterSpeedLimit#forceLow"), false);
    self.speedLimitLow = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.cutterSpeedLimit#low"), 12);
    self.speedLimit = Utils.getNoNil(getXMLFloat(xmlFile, "vehicle.cutterSpeedLimit#normal"), 17.5);

    self.speedViolationMaxTime = 50;
    self.speedViolationTimer = self.speedViolationMaxTime;


    self.lastCutterArea = 0;
    self.lastCutterAreaBiggerZero = self.lastCutterArea > 0;

    self.cutterGroundFlag = self:getNextDirtyFlag();

    self.aiLeftCheck = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.aiLeftCheck#index"));
    self.aiRightCheck = Utils.indexToObject(self.components, getXMLString(xmlFile, "vehicle.aiRightCheck#index"));

    self.dtSum = 0;
	
    self.combineGrainTankFruitTypes = {};
    self.combineGrainTankFruitTypesAdd = {};

    local fruitTypes = getXMLString(xmlFile, "vehicle.combineGrainTankFruitTypes#fruitTypes");
    if fruitTypes ~= nil then
        local types = Utils.splitString(" ", fruitTypes);
        for k,v in pairs(types) do
            local desc = FruitUtil.fruitTypes[v];
            if desc ~= nil then
                self.combineGrainTankFruitTypes[desc.index] = true;
			else
				if v == "grass_windrow" then
					local descindex = Fillable.fillTypeNameToInt[v];
					if descindex ~= nil then
						self.combineGrainTankFruitTypes[descindex] = true;
					end
				end
            end;
        end;
    end;
	
	self.grassForceGrass = false
	self.fixCutterGroundDistance = false
	
    self.particleSystems = {};
	local i = 0;
	while true do
		local keyPS = string.format("vehicle.particleSystems.threshingParticleSystem(%d)", i);
		local t = getXMLString(xmlFile, keyPS .. "#type");
		if t == nil then
			break;
		end;
		local fillType = Fillable.fillTypeNameToInt[t];
		if fillType ~= nil then
			local currentPS = {};
			local particleNode = Utils.loadParticleSystem(xmlFile, currentPS, keyPS, self.components, false, nil, self.baseDirectory);
			self.particleSystems[fillType] = currentPS;
            if self.defaultParticleSystems == nil then
                self.defaultParticleSystems = currentPS;
            end;
			
		end;
		i = i + 1;
	end;
	
    self.particleSystemsDeactivateTime = -1;
	
end;

function EasyFlow:postLoad(xmlFile)
	-- fix errors for grass_windrow, fruit not safe but works fine
	local fruitName = "grass_windrow";
	local fruitIndex = Fillable.fillTypeNameToInt[fruitName]

	FruitUtil.fruitIndexToDesc[fruitIndex] = {}
	FruitUtil.fruitIndexToDesc[fruitIndex].index = fruitIndex
	FruitUtil.fruitIndexToDesc[fruitIndex].hasWindrow = false
	FruitUtil.fruitIndexToDesc[fruitIndex].name = fruitName
	FruitUtil.fruitIndexToDesc[fruitIndex].literPerSqm = 1.2	
end

function EasyFlow:applyInitialAnimation(superFunc)
    if self.cutterStartAnimation ~= nil and self.playAnimation ~= nil and self.cutterStartAnimationInitialIsStarted then
        self:playAnimation(self.cutterStartAnimation, -self.cutterStartAnimationSpeedScale, nil, true);
        AnimatedVehicle.updateAnimations(self, 99999999);
    end
    if superFunc ~= nil then
        superFunc(self);
    end
end

function EasyFlow:delete()
    Utils.deleteParticleSystem(self.threshingParticleSystems);
	for _, particleSystem in pairs(self.particleSystems) do
		Utils.deleteParticleSystem(particleSystem);
	end
end;

function EasyFlow:readStream(streamId, timestamp, connection)
    self.lastCutterAreaBiggerZero = streamReadBool(streamId);
end;

function EasyFlow:writeStream(streamId, connection, dirtyMask)
    streamWriteBool(streamId, self.lastCutterAreaBiggerZero);
end;

function EasyFlow:readUpdateStream(streamId, timestamp, connection)
    if connection:getIsServer() then
        self.lastCutterAreaBiggerZero = streamReadBool(streamId);
    end;
end;

function EasyFlow:writeUpdateStream(streamId, connection, dirtyMask)
    if not connection:getIsServer() then
        streamWriteBool(streamId, self.lastCutterAreaBiggerZero);
    end;
end;

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

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

function EasyFlow:update(dt)
	local combineAI = self:getCombine();

    if self:getIsActiveForInput(false) then
		if combineAI ~= nil then		
			if not combineAI.isHired then
				if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA4) then
					self:changeGrassForceGrass(not self.grassForceGrass);
				end;
			end;
		end
	end
	
	Utils.setEmittingState(self.threshingParticleSystems, (self.reelStarted and self.lastCutterAreaBiggerZero));

	if self.reelStarted and self.lastCutterAreaBiggerZero then
		self.particleSystemsDeactivateTime = self.time + 180;
		local currentParticleSystems =  self.particleSystems[self.currentInputFruitType];
		if currentParticleSystems == nil then
			currentParticleSystems = self.defaultParticleSystems;
		end;
		if currentParticleSystems ~= self.currentParticleSystems then
			Utils.setEmittingState(self.currentParticleSystems, false);
			self.currentParticleSystems = currentParticleSystems;
			Utils.setEmittingState(self.currentParticleSystems, true);
		end
	else
		if self.particleSystemsDeactivateTime >= 0 and self.particleSystemsDeactivateTime <= self.time then
			self.particleSystemsDeactivateTime = -1;
			if self.currentParticleSystems ~= nil then
				Utils.setEmittingState(self.currentParticleSystems, false);
				self.currentParticleSystems = nil;
			end
		end
	end
	
    if self.reelStarted then

        for _, rollNode in pairs(self.rollNodes) do
            rotate(rollNode.node, -dt*rollNode.speed, 0, 0);
        end;

        if self.reelNode ~= nil then
            rotate(self.reelNode, -dt*self.reelSpeed*self.reelSpeedScale, 0, 0);

            if self.sideArmMovable then
                --:TODO: move arm downwards
            end;

            --correct spikes, so that they always look down
            local atx, aty, atz = getRotation(self.reelNode);

            for i=1, self.spikesCount do
                local spike = getChildAt(self.spikesRootNode, i-1);
                --local tx, ty, tz = getRotation(spike);
                setRotation(spike, -atx, aty, atz);
            end;
        end;
    end;
	
	if self.grassForceGrass == true and (combineAI.lastValidGrainTankFruitType == Fillable.fillTypeNameToInt["grass"] or combineAI.lastValidGrainTankFruitType == Fillable.fillTypeNameToInt["grass_windrow"])then
		self:combineAIupdateGrass(dt)
	else
		self:combineAIupdateChaff(dt)
	end
	
end;

function EasyFlow:combineAIupdateGrass(dt)
	local combineAI = self:getCombine();
	
	if combineAI ~= nil then		
		local doAutoAiming = combineAI.pipeStateIsAutoAiming[combineAI.currentPipeState];
		local targetTrailer = nil;
		local outputType = ""
		local outputDesc = combineAI.lastValidGrainTankFruitType
		if doAutoAiming then
			targetTrailer = combineAI:findAutoAimTrailerToUnload(outputDesc);
			if targetTrailer == nil then
				outputType = "grass"
				outputDesc = Fillable.fillTypeNameToInt[outputType];
				targetTrailer = EasyFlow.findAutoAimTrailerToUnload(combineAI, outputDesc);
				if targetTrailer == nil then
					outputType = "grass_windrow"
					outputDesc = Fillable.fillTypeNameToInt[outputType];
					targetTrailer = EasyFlow.findAutoAimTrailerToUnload(combineAI, outputDesc);
				end
			end;
		end;
		if targetTrailer == nil then
			doAutoAiming = false;
		end
		
		if (combineAI.currentPipeState ~= combineAI.targetPipeState or doAutoAiming) and combineAI.targetPipeState <= combineAI.numPipeStates then
			local autoAimX, autoAimY, autoAimZ;
			if doAutoAiming then
				autoAimX, autoAimY, autoAimZ = getWorldTranslation(targetTrailer.fillAutoAimTargetNode);
			end;
			local moved = false;
			for i=1, table.getn(combineAI.pipeNodes) do
				local nodeMoved = false;
				local pipeNode = combineAI.pipeNodes[i];

				local state = pipeNode.states[combineAI.targetPipeState];
				if pipeNode.translationSpeeds ~= nil then
					for i=1, 3 do
						if pipeNode.curTranslation[i] ~= state.translation[i] then
							nodeMoved = true;
							if pipeNode.curTranslation[i] < state.translation[i] then
								pipeNode.curTranslation[i] = math.min(pipeNode.curTranslation[i] + dt*pipeNode.translationSpeeds[i], state.translation[i]);
							else
								pipeNode.curTranslation[i] = math.max(pipeNode.curTranslation[i] - dt*pipeNode.translationSpeeds[i], state.translation[i]);
							end;
						end;
					end;
					setTranslation(pipeNode.node, pipeNode.curTranslation[1],pipeNode.curTranslation[2],pipeNode.curTranslation[3])
				end;
				if pipeNode.rotationSpeeds ~= nil then
					for i=1, 3 do
						local targetRotation = state.rotation[i];
						if doAutoAiming then
							if pipeNode.autoAimXRotation and i == 1 then
								local x,y,z = getWorldTranslation(pipeNode.node);
								local x,y,z = worldDirectionToLocal(getParent(pipeNode.node), autoAimX-x, autoAimY-y, autoAimZ-z);
								targetRotation = -math.atan2(y,z);
								if pipeNode.autoAimInvertZ then
									targetRotation = targetRotation+math.pi;
								end;
								targetRotation = Utils.normalizeRotationForShortestPath(targetRotation, pipeNode.curRotation[i]);
							elseif pipeNode.autoAimYRotation and i == 2 then
								local x,y,z = getWorldTranslation(pipeNode.node);
								local x,y,z = worldDirectionToLocal(getParent(pipeNode.node), autoAimX-x, autoAimY-y, autoAimZ-z);
								targetRotation = math.atan2(x,z);
								if pipeNode.autoAimInvertZ then
									targetRotation = targetRotation+math.pi;
								end;
								targetRotation = Utils.normalizeRotationForShortestPath(targetRotation, pipeNode.curRotation[i]);
							end;
						end;
						if pipeNode.curRotation[i] ~= targetRotation then
							nodeMoved = true;
							if pipeNode.curRotation[i] < targetRotation then
								pipeNode.curRotation[i] = math.min(pipeNode.curRotation[i] + dt*pipeNode.rotationSpeeds[i], targetRotation);
							else
								pipeNode.curRotation[i] = math.max(pipeNode.curRotation[i] - dt*pipeNode.rotationSpeeds[i], targetRotation);
							end;
						end;
					end;
					setRotation(pipeNode.node, pipeNode.curRotation[1],pipeNode.curRotation[2],pipeNode.curRotation[3])
				end;
				moved = moved or nodeMoved;

				if nodeMoved and combineAI.setMovingToolDirty ~= nil then
					combineAI:setMovingToolDirty(pipeNode.node);
				end;
			end;
			if not moved then
				combineAI.currentPipeState = combineAI.targetPipeState;
			end;
		end;
	end;
end

function EasyFlow:combineAIupdateChaff(dt)
end

function EasyFlow:combineAIupdateTickGrass(dt)
	local combineAI = self:getCombine();
	
	if combineAI ~= nil then	
		if self.reelStarted and self.movingDirection == self.cutterMovingDirection and (self.cutterAllowCuttingWhileRaised or self:isLowered(true)) then
			local speedLimit = self.speedLimit;
			if EasyFlow.getUseLowSpeedLimit(self) then
				speedLimit = self.speedLimitLow;
			end

			if self:doCheckSpeedLimit() and self.lastSpeed*3600 > speedLimit then
				self.speedViolationTimer = self.speedViolationTimer - dt;
			else
				self.speedViolationTimer = self.speedViolationMaxTime;
			end
			local oldInputFruitType = self.currentInputFruitType;
			if self.isServer then

				if self.speedViolationTimer > 0 then
					if combineAI ~= nil and combineAI:getIsThreshingAllowed(false) and combineAI.allowsThreshing then

						local cuttingAreasSend = {};
						for _,area in pairs(self.cuttingAreas) do
							if self:getIsAreaActive(area) then
								local x,_,z = getWorldTranslation(area.start);
								local x1,_,z1 = getWorldTranslation(area.width);
								local x2,_,z2 = getWorldTranslation(area.height);
								table.insert(cuttingAreasSend, {x,z,x1,z1,x2,z2});
							end;
						end;
						if (table.getn(cuttingAreasSend) > 0) then
							for fruitType,_ in pairs(self.fruitTypes) do
								local outputFruitType = fruitType;
								if self.convertedFruits[fruitType] ~= nil then
									outputFruitType = self.convertedFruits[fruitType];
								end;
								if combineAI:getIsCutterFruitTypeAllowed(outputFruitType) then
									local lastCutterArea, realArea = EasyFlowAreaEvent.runLocally(cuttingAreasSend, fruitType);
									if lastCutterArea > 0 then
										self.currentInputFruitType = fruitType;

										self.lastCutterArea = lastCutterArea;
										self.lastCutterAreaBiggerZero = (self.lastCutterArea > 0);

										-- combineAI:addCutterArea(self, lastCutterArea, realArea, fruitType, outputFruitType);
										if combineAI.lastCuttersFruitType == FruitUtil.FRUITTYPE_UNKNOWN or combineAI.lastCuttersArea == 0 or combineAI.lastCuttersFruitType == fruitType_outputFruitType then
											combineAI.lastCuttersArea = combineAI.lastCuttersArea + lastCutterArea;
											combineAI.lastCuttersFruitType = outputFruitType;
											combineAI.lastCuttersInputFruitType = fruitType;
											combineAI.lastCuttersAreaTime = combineAI.time;

											-- 8000/1200 = 6.66 liter/meter
											-- 8000/1200 / 6 = 1.111 liter/m^2
											-- 8000/1200 / 6 / 2^2 = 0.277777 liter / density pixel (density is 4096^2, on a area of 2048m^2
											local pixelToSqm = g_currentMission:getFruitPixelsToSqm(); -- 4096px are mapped to 2048m
											local literPerSqm = 1;
											if (fruitType ~= FruitUtil.FRUITTYPE_UNKNOWN) then
												literPerSqm = FruitUtil.fruitIndexToDesc[fruitType].literPerSqm; -- * (1 + 0.5 * (3 - g_currentMission.missionStats.difficulty));
												if (fruitType ~= outputFruitType and fruitType ~= FruitUtil.FRUITTYPE_UNKNOWN) then												
													literPerSqm = literPerSqm * FruitUtil.fruitIndexToDesc[Fillable.fillTypeNameToInt["chaff"]].literPerSqm;
												end;
											end;

											local sqm = lastCutterArea*pixelToSqm;

											local deltaFillLevel = sqm*literPerSqm*combineAI.threshingScale;

											local newFillLevel = combineAI.grainTankFillLevel+deltaFillLevel;
												
											combineAI:setGrainTankFillLevel(newFillLevel, outputFruitType);
										end
										
										g_server:broadcastEvent(EasyFlowAreaEvent:new(cuttingAreasSend, fruitType));

										if self.lastCutterAreaBiggerZero ~= self.lastCutterAreaBiggerZeroSent then
											self:raiseDirtyFlags(self.cutterGroundFlag);
											self.lastCutterAreaBiggerZeroSent = self.lastCutterAreaBiggerZero;
										end;
										local pixelToSqm = g_currentMission:getFruitPixelsToSqm(); -- 4096px are mapped to 2048m
										local sqm = realArea*pixelToSqm;
										local ha = sqm/10000;

										g_currentMission.missionStats.hectaresThreshedTotal = g_currentMission.missionStats.hectaresThreshedTotal + ha;
										g_currentMission.missionStats.hectaresThreshedSession = g_currentMission.missionStats.hectaresThreshedSession + ha;

										-- Stop the loop over all fruit types
										break;
									end
								end
							end
						end
					end
				end
			else
				if combineAI ~= nil and combineAI.lastValidInputFruitType ~= FruitUtil.FRUITTYPE_UNKNOWN then
					self.currentInputFruitType = combineAI.lastValidInputFruitType;
				end
			end
			if self.currentInputFruitType ~= oldInputFruitType then
				EasyFlow.updateExtraObjects(self);
			end

			if self:isLowered(true) then
				local foldAnimTime = self.foldAnimTime;
				for k,v in pairs(self.cutterSpeedRotatingParts) do
					if foldAnimTime == nil or (foldAnimTime <= v.foldMaxLimit and foldAnimTime >= v.foldMinLimit) then
						rotate(v.node, v.rotationSpeedScale * self.lastSpeedReal * self.movingDirection * dt, 0, 0);
					end
				end
			end
		else
			self.speedViolationTimer = self.speedViolationMaxTime;
		end
		
		if self.isServer then
			combineAI.pipeIsUnloading = false;
			if combineAI.pipeStateIsUnloading[combineAI.currentPipeState] and combineAI:getIsPipeUnloadingAllowed() then
				if combineAI.grainTankFillLevel > 0 then

					-- test if we should drain the grain tank
					local trailer, trailerDistance = EasyFlow.findTrailerToUnload(combineAI, combineAI.currentGrainTankFruitType);
					if trailer == nil then
						combineAI.pipeIsUnloading = false;
					else
						trailer:resetFillLevelIfNeeded(combineAI.currentGrainTankFruitType);

						local deltaLevel = combineAI.grainTankFillLevel;
						
						deltaLevel = math.min(deltaLevel, trailer.capacity - trailer.fillLevel);
						if deltaLevel > 0 then
							combineAI.pipeIsUnloading = true;
							combineAI.pipeUnloadingDistance = trailerDistance;

							trailer:setFillLevel(trailer.fillLevel+deltaLevel, combineAI.currentGrainTankFruitType);
							combineAI:setGrainTankFillLevel(combineAI.grainTankFillLevel-deltaLevel, combineAI.currentGrainTankFruitType);
						end
					end;
				end;
			end;
		end;
		
		if self.isClient then
			if combineAI.pipeIsUnloading or (combineAI.combineIsFilling and combineAI.pipeUnloadingDistance ~= nil )then
				combineAI.pipeParticleDeactivateTime = combineAI.time + 222;
				local currentPipeParticleSystem = combineAI.pipeParticleSystems[combineAI.lastValidGrainTankFruitType];
				if currentPipeParticleSystem == nil then
					currentPipeParticleSystem = combineAI.defaultPipeParticleSystem;
				end;
				if currentPipeParticleSystem ~= combineAI.currentPipeParticleSystem then
					Utils.setEmittingState(combineAI.currentPipeParticleSystem, false);

					combineAI.currentPipeParticleSystem = currentPipeParticleSystem;
					Utils.setEmittingState(combineAI.currentPipeParticleSystem, true);
				end

				if combineAI.currentPipeParticleSystem ~= nil then
					for _, v in ipairs(combineAI.currentPipeParticleSystem) do
						local pipeUnloadingDistance = v.originalLifespan
						if combineAI.pipeUnloadingDistance ~= nil then
							pipeUnloadingDistance = combineAI.pipeUnloadingDistance
						end
						local lifespan = math.min(v.originalLifespan, (pipeUnloadingDistance+combineAI.pipeParticleSystemExtraDistance)/v.speed);
						setParticleSystemLifespan(v.geometry, lifespan, true);
					end
				end

				local currentPipeScrollers = combineAI.pipeScrollers[combineAI.lastValidGrainTankFruitType];
				if currentPipeScrollers ~= combineAI.currentPipeScrollers and combineAI.currentPipeScrollers ~= nil then
					for _, scroller in pairs(combineAI.currentPipeScrollers) do
						if scroller.toggleVisibility then
							setVisibility(scroller.node, false);
						end
					end
				end
				combineAI.currentPipeScrollers = currentPipeScrollers;
				if combineAI.currentPipeScrollers ~= nil then
					for _, scroller in pairs(combineAI.currentPipeScrollers) do
						scroller.scrollPosition = (scroller.scrollPosition + dt*scroller.scrollSpeed) % scroller.scrollLength;
						if scroller.shaderParameterComponent == 1 then
							setShaderParameter(scroller.node, scroller.shaderParameterName, scroller.scrollPosition,0,0,0, false);
						else
							setShaderParameter(scroller.node, scroller.shaderParameterName, 0,scroller.scrollPosition,0,0, false);
						end
						if scroller.toggleVisibility then
							setVisibility(scroller.node, true);
						end
					end
				end
			else
				if combineAI.pipeParticleDeactivateTime >= 0 and combineAI.pipeParticleDeactivateTime <= combineAI.time then
					combineAI.pipeParticleDeactivateTime = -1;
					if combineAI.currentPipeParticleSystem ~= nil then
						Utils.setEmittingState(combineAI.currentPipeParticleSystem, false);
						combineAI.currentPipeParticleSystem = nil;
					end

					if combineAI.currentPipeScrollers ~= nil then
						for _, scroller in pairs(combineAI.currentPipeScrollers) do
							if scroller.toggleVisibility then
								setVisibility(scroller.node, false);
							end
						end
						combineAI.currentPipeScrollers = nil
					end
				end
			end
		end
	end
end

function EasyFlow:combineAIupdateTickChaff(dt)
	local combineAI = self:getCombine();
	
	if combineAI ~= nil then	
		if self.reelStarted and self.movingDirection == self.cutterMovingDirection and (self.cutterAllowCuttingWhileRaised or self:isLowered(true)) then
			local speedLimit = self.speedLimit;
			if EasyFlow.getUseLowSpeedLimit(self) then
				speedLimit = self.speedLimitLow;
			end

			if self:doCheckSpeedLimit() and self.lastSpeed*3600 > speedLimit then
				self.speedViolationTimer = self.speedViolationTimer - dt;
			else
				self.speedViolationTimer = self.speedViolationMaxTime;
			end
			local oldInputFruitType = self.currentInputFruitType;
			if self.isServer then

				if self.speedViolationTimer > 0 then
					if combineAI ~= nil and combineAI:getIsThreshingAllowed(false) and combineAI.allowsThreshing then

						local cuttingAreasSend = {};
						for _,area in pairs(self.cuttingAreas) do
							if self:getIsAreaActive(area) then
								local x,_,z = getWorldTranslation(area.start);
								local x1,_,z1 = getWorldTranslation(area.width);
								local x2,_,z2 = getWorldTranslation(area.height);
								table.insert(cuttingAreasSend, {x,z,x1,z1,x2,z2});
							end;
						end;
						if (table.getn(cuttingAreasSend) > 0) then
							for fruitType,_ in pairs(self.fruitTypes) do
								local outputFruitType = fruitType;
								if self.convertedFruits[fruitType] ~= nil then
									outputFruitType = self.convertedFruits[fruitType];
								end;
								if combineAI:getIsCutterFruitTypeAllowed(outputFruitType) then

									local lastCutterArea, realArea = EasyFlowAreaEvent.runLocally(cuttingAreasSend, fruitType);
									if lastCutterArea > 0 then
										self.currentInputFruitType = fruitType;

										self.lastCutterArea = lastCutterArea;
										self.lastCutterAreaBiggerZero = (self.lastCutterArea > 0);

										combineAI:addCutterArea(self, lastCutterArea, realArea, fruitType, outputFruitType);

										g_server:broadcastEvent(EasyFlowAreaEvent:new(cuttingAreasSend, fruitType));

										if self.lastCutterAreaBiggerZero ~= self.lastCutterAreaBiggerZeroSent then
											self:raiseDirtyFlags(self.cutterGroundFlag);
											self.lastCutterAreaBiggerZeroSent = self.lastCutterAreaBiggerZero;
										end;
										local pixelToSqm = g_currentMission:getFruitPixelsToSqm(); -- 4096px are mapped to 2048m
										local sqm = realArea*pixelToSqm;
										local ha = sqm/10000;

										g_currentMission.missionStats.hectaresThreshedTotal = g_currentMission.missionStats.hectaresThreshedTotal + ha;
										g_currentMission.missionStats.hectaresThreshedSession = g_currentMission.missionStats.hectaresThreshedSession + ha;

										-- Stop the loop over all fruit types
										break;
									end
								end
							end
						end
					end
				end
			else
				if combineAI ~= nil and combineAI.lastValidInputFruitType ~= FruitUtil.FRUITTYPE_UNKNOWN then
					self.currentInputFruitType = combineAI.lastValidInputFruitType;
				end
			end
			if self.currentInputFruitType ~= oldInputFruitType then
				EasyFlow.updateExtraObjects(self);
			end

			if self:isLowered(true) then
				local foldAnimTime = self.foldAnimTime;
				for k,v in pairs(self.cutterSpeedRotatingParts) do
					if foldAnimTime == nil or (foldAnimTime <= v.foldMaxLimit and foldAnimTime >= v.foldMinLimit) then
						rotate(v.node, v.rotationSpeedScale * self.lastSpeedReal * self.movingDirection * dt, 0, 0);
					end
				end
			end
		else
			self.speedViolationTimer = self.speedViolationMaxTime;
		end
	end
end

function EasyFlow:updateTick(dt)
	local combineAI = self:getCombine();
	
	if combineAI ~= nil then		
		combineAI.grassForceGrass = self.grassForceGrass
		
		if self.isServer then
			self.lastCutterArea = 0;
			self.lastCutterAreaBiggerZero = false;
		
			-- follow ground distance
			if self:isLowered(true) then
				local rax,ray,raz = getTranslation(self.components[2].node)
				local rbx,rby,rbz = getWorldTranslation(self.componentJoints[1].jointNode)
				local rdy = rby - ray - 0.445
				local lax,lay,laz = getTranslation(self.components[3].node)
				local lbx,lby,lbz = getWorldTranslation(self.componentJoints[2].jointNode)
				local ldy = lby - lay - 0.445
				if self.fixCutterGroundDistance == false and rdy < -0.01 and ldy < -0.01 then
					self.fixCutterGroundDistance = true
				end
				local implement = combineAI:getImplementByObject(self);
				if implement ~= nil and self.fixCutterGroundDistance == true then
					local jointDesc = combineAI.attacherJoints[implement.jointDescIndex];
					if jointDesc ~= nil then
						local node1RotX, _, _ = getRotation(jointDesc.rotationNode)
						local node2RotX, _, _ = getRotation(jointDesc.rotationNode2)
						if rdy + ldy < -0.01 or rdy + ldy > 0.01 then
							node1RotX = node1RotX + ((rdy + ldy) * 0.075)
							node2RotX = node2RotX - ((rdy + ldy) * 0.075)
							if node1RotX > jointDesc.maxRot[1] then
								node1RotX = jointDesc.maxRot[1]
							end
							if node1RotX < jointDesc.minRot[1] then
								node1RotX = jointDesc.minRot[1]
							end
							if node2RotX < jointDesc.maxRot2[1] then
								node2RotX = jointDesc.maxRot2[1]
							end
							if node2RotX > jointDesc.minRot2[1] then
								node2RotX = jointDesc.minRot2[1]
							end
							setRotation(jointDesc.rotationNode, node1RotX, 0, 0)
							setRotation(jointDesc.rotationNode2, node2RotX, 0, 0)
							setJointFrame(jointDesc.jointIndex, 0, jointDesc.jointTransform);
						end
					end;
				end;
			else
				self.fixCutterGroundDistance = false
			end;
			
			-- some combine variables
			if combineAI.aiLeftCheck == nil or combineAI.aiRightCheck == nil then
				combineAI.aiLeftCheck = self.aiLeftCheck
				combineAI.aiRightCheck = self.aiRightCheck
			end
			
			-- cutter drives combine :)
			if combineAI.isAIThreshing then
				if combineAI.isBroken or combineAI.aiLeftCheck == nil or combineAI.aiRightCheck == nil then
					combineAI:stopAIThreshing();
				end;
				combineAI.dtSum = - 1000
				self.dtSum = self.dtSum + dt;
				if self.dtSum > 50 then
					EasyFlow.updateAIMovement(combineAI, self.dtSum);
					self.dtSum = 0;
				end;
			end
		end
		if self.grassForceGrass == true and (combineAI.lastValidGrainTankFruitType == Fillable.fillTypeNameToInt["grass"] or combineAI.lastValidGrainTankFruitType == Fillable.fillTypeNameToInt["grass_windrow"])then
			self:combineAIupdateTickGrass(dt)
		else
			self:combineAIupdateTickChaff(dt)
		end
    end;
end

function EasyFlow.updateAIMovement(self, dt)

    if not self:getIsAIThreshingAllowed() then
        self:stopAIThreshing();
        return;
    end;

    if not self.isControlled then
        if g_currentMission.environment.needsLights then
            self:setLightsVisibility(true);
        else
            self:setLightsVisibility(false);
        end;
    end;

    local allowedToDrive = true;
    if self.grainTankCapacity == 0 then
        if not self.pipeStateIsUnloading[self.currentPipeState] then
            allowedToDrive = false;
        end
        if not self.isPipeUnloading and (self.lastArea > 0 or self.lastLostGrainTankFillLevel > 0) then
            -- there is some fruit to unload, but there is no trailer. Stop and wait for a trailer
            self.waitingForTrailerToUnload = true;
        end;
    else
        if self.grainTankFillLevel >= self.grainTankCapacity then
            allowedToDrive = false;
        end
    end

    if self.waitingForTrailerToUnload then
        if self.lastValidGrainTankFruitType ~= FruitUtil.FRUITTYPE_UNKNOWN then
			local outputType = ""
			local outputDesc = self.lastValidGrainTankFruitType
			local trailer, trailerDistance = self:findTrailerToUnload(outputDesc);
			if self.grassForceGrass == true then
				outputType = "grass"
				outputDesc = Fillable.fillTypeNameToInt[outputType];
				trailer, trailerDistance  = EasyFlow.findTrailerToUnload(self, outputDesc);
				if trailer == nil then
					outputType = "grass_windrow"
					outputDesc = Fillable.fillTypeNameToInt[outputType];
					trailer, trailerDistance  = EasyFlow.findTrailerToUnload(self, outputDesc);
				end
			end;
            if trailer ~= nil then
                -- there is a trailer to unload. Continue working
                self.waitingForTrailerToUnload = false;
            end;
        else
            -- we did not cut anything yet. We shouldn't have ended in this state. Just continue working
            self.waitingForTrailerToUnload = false;
        end;
    end;

    if (self.grainTankFillLevel >= self.grainTankCapacity and self.grainTankCapacity > 0) or self.waitingForTrailerToUnload or self.waitingForDischarge then
		allowedToDrive = false;
    end;
    for _,v in pairs(self.numCollidingVehicles) do
        if v > 0 then
            allowedToDrive = false;
            break;
        end;
    end;
    if self.turnStage > 0 then
        if self.waitForTurnTime > self.time or (self.pipeIsUnloading and self.turnStage < 3) then
            allowedToDrive = false;
        end;
    end;
    if not self:getIsThreshingAllowed(true) then
        allowedToDrive = false;
        self:setIsThreshing(false);
        self.waitingForWeather = true;
    else
        if self.waitingForWeather then
            if self.turnStage == 0 then
                self.driveBackTime = self.time + self.driveBackTimeout;
            end;
            self:startThreshing();
            self.waitingForWeather = false;
        end;
    end;
    if not allowedToDrive then
        --local x,y,z = getWorldTranslation(self.aiTreshingDirectionNode);
        --local lx, lz = 0, 1; --AIVehicleUtil.getDriveDirection(self.aiTreshingDirectionNode, self.aiThreshingTargetX, y, self.aiThreshingTargetZ);
        --AIVehicleUtil.driveInDirection(self, dt, 30, 0, 0, 28, false, moveForwards, lx, lz)
        AIVehicleUtil.driveInDirection(self, dt, 30, 0, 0, 28, false, moveForwards, nil, nil)
        return;
    end;

    local speedLevel = 2;

    local leftMarker = self.aiLeftMarker;
    local rightMarker = self.aiRightMarker;
    local hasFruitPreparer = false;
    local fruitType = self.lastValidInputFruitType;
	
	if self.grassForceGrass == true then
		outputType = "grass"
		fruitType = Fillable.fillTypeNameToInt[outputType];
	end
	
    if self.fruitPreparerFruitType ~= nil and self.fruitPreparerFruitType == fruitType then
        hasFruitPreparer = true;
    end
    for cutter,implement in pairs(self.attachedCutters) do
        if cutter.aiLeftMarker ~= nil and leftMarker == nil then
            leftMarker = cutter.aiLeftMarker;
        end;
        if cutter.aiRightMarker ~= nil and rightMarker == nil then
            rightMarker = cutter.aiRightMarker;
        end;
        if Cutter.getUseLowSpeedLimit(cutter) then
            speedLevel = 1;
        end;
    end;

    if leftMarker == nil or rightMarker == nil then
       self:stopAIThreshing();
        return;
    end;

    if self.driveBackTime >= self.time then
        local x,y,z = getWorldTranslation(self.aiTreshingDirectionNode);
        local lx, lz = AIVehicleUtil.getDriveDirection(self.aiTreshingDirectionNode, self.aiThreshingTargetX, y, self.aiThreshingTargetZ);
        AIVehicleUtil.driveInDirection(self, dt, 30, 0, 0, 28, true, false, lx, lz, speedLevel, 1)
        return;
    end;

    local hasArea = true;
    if self.lastArea < 1 then
        local x,y,z = getWorldTranslation(self.aiTreshingDirectionNode);
        local dirX, dirZ = self.aiThreshingDirectionX, self.aiThreshingDirectionZ;
        local lInX,  lInY,  lInZ = getWorldTranslation(leftMarker);
        local rInX,  rInY,  rInZ = getWorldTranslation(rightMarker);

        local heightX = lInX + dirX * self.frontAreaSize;
        local heightZ = lInZ + dirZ * self.frontAreaSize;

        -- local area = Utils.getFruitArea(fruitType, lInX, lInZ, rInX, rInZ, heightX, heightZ, hasFruitPreparer);
        local area = EasyFlow.getFruitWindrowArea(fruitType, lInX, lInZ, rInX, rInZ, heightX, heightZ);
        if area < 1 then
            hasArea = false;
        end;
    end;
    if hasArea then
		self.turnTimer = self.turnTimeout;
    else
        self.turnTimer = self.turnTimer - dt;
    end;


    local newTargetX, newTargetY, newTargetZ;

    local moveForwards = true;
    local updateWheels = true;
	self.moveForwards = false

    if self.turnTimer < 0 or (self.turnStage > 0 or self.turnStage < 0) then
        if self.turnStage > 0 then
            local x,y,z = getWorldTranslation(self.aiTreshingDirectionNode);
            local dirX, dirZ = self.aiThreshingDirectionX, self.aiThreshingDirectionZ;
            local myDirX, myDirY, myDirZ = localDirectionToWorld(self.aiTreshingDirectionNode, 0, 0, 1);

            newTargetX = self.aiThreshingTargetX;
            newTargetY = y;
            newTargetZ = self.aiThreshingTargetZ;
			
            if self.turnStage == 1 then
                self.turnStageTimer = self.turnStageTimer - dt;
                if self.lastSpeed < self.aiRescueSpeedThreshold then
                    self.aiRescueTimer = self.aiRescueTimer - dt;
                else
                    self.aiRescueTimer = self.aiRescueTimeout;
                end;
                if myDirX*dirX + myDirZ*dirZ > self.turnStage1AngleCosThreshold or self.turnStageTimer < 0 or self.aiRescueTimer < 0 then
				self.turnStage = 2;
                    moveForwards = false;
                    if self.turnStageTimer < 0 or self.aiRescueTimer < 0 then

                        self.aiThreshingTargetBeforeSaveX = self.aiThreshingTargetX;
                        self.aiThreshingTargetBeforeSaveZ = self.aiThreshingTargetZ;

                        newTargetX = self.aiThreshingTargetBeforeTurnX;
                        newTargetZ = self.aiThreshingTargetBeforeTurnZ;

                        moveForwards = false;
                        self.turnStage = 4;
                        self.turnStageTimer = self.turnStage4Timeout;
                    else
                        self.turnStageTimer = self.turnStage2Timeout;
                    end;
                    self.aiRescueTimer = self.aiRescueTimeout;
                end;
            elseif self.turnStage == 2 then
                self.turnStageTimer = self.turnStageTimer - dt;
                if self.lastSpeed < self.aiRescueSpeedThreshold then
                    self.aiRescueTimer = self.aiRescueTimer - dt;
                else
                    self.aiRescueTimer = self.aiRescueTimeout;
                end;
                if (myDirX*dirX + myDirZ*dirZ > self.turnStage2AngleCosThreshold or self.turnStageTimer < 0 or self.aiRescueTimer < 0) or self.skipTurnStage2 == true then
                    AICombine.switchToTurnStage3(self);
                else
                    moveForwards = false;
                end;
            elseif self.turnStage == 3 then
                --[[if Utils.vector2Length(x-newTargetX, z-newTargetZ) < self.turnEndDistance then
                    self.turnTimer = self.turnTimeoutLong;
                    self.turnStage = 0;
                    --print("turning done");
                end;]]
                if self.lastSpeed < self.aiRescueSpeedThreshold then
                    self.aiRescueTimer = self.aiRescueTimer - dt;
                else
                    self.aiRescueTimer = self.aiRescueTimeout;
                end;
                local dx, dz = x-newTargetX, z-newTargetZ;
                local dot = dx*dirX + dz*dirZ;
                if -dot < self.turnEndDistance then
                    self.turnTimer = self.turnTimeoutLong;
                    self.turnStage = 0;
                elseif self.aiRescueTimer < 0 then
                    self.aiThreshingTargetBeforeSaveX = self.aiThreshingTargetX;
                    self.aiThreshingTargetBeforeSaveZ = self.aiThreshingTargetZ;

                    newTargetX = self.aiThreshingTargetBeforeTurnX;
                    newTargetZ = self.aiThreshingTargetBeforeTurnZ;

                    moveForwards = false;
                    self.turnStage = 4;
                    self.turnStageTimer = self.turnStage4Timeout;
                end;
            elseif self.turnStage == 4 then
			
                self.turnStageTimer = self.turnStageTimer - dt;
                if self.lastSpeed < self.aiRescueSpeedThreshold then
                    self.aiRescueTimer = self.aiRescueTimer - dt;
                else
                    self.aiRescueTimer = self.aiRescueTimeout;
                end;
                if self.aiRescueTimer < 0 then
                    self.aiRescueTimer = self.aiRescueTimeout;
                    local x,y,z = localDirectionToWorld(self.aiRescueNode, 0, 0, -1);
                    local scale = self.aiRescueForce/Utils.vector2Length(x,z);
                    addForce(self.aiRescueNode, x*scale, 0, z*scale, 0, 0, 0, true);
                end;
                if self.turnStageTimer < 0 then
                    self.aiRescueTimer = self.aiRescueTimeout;
                    self.turnStageTimer = self.turnStage1Timeout;
                    self.turnStage = 1;

                    newTargetX = self.aiThreshingTargetBeforeSaveX;
                    newTargetZ = self.aiThreshingTargetBeforeSaveZ;
                else
                    local dirX, dirZ = -dirX, -dirZ;
                    -- just drive along direction
                    local targetX, targetZ = self.aiThreshingTargetX, self.aiThreshingTargetZ;
                    local dx, dz = x-targetX, z-targetZ;
                    local dot = dx*dirX + dz*dirZ;

                    local projTargetX = targetX +dirX*dot;
                    local projTargetZ = targetZ +dirZ*dot;

                    newTargetX = projTargetX-dirX*self.lookAheadDistance;
                    newTargetZ = projTargetZ-dirZ*self.lookAheadDistance;
                    moveForwards = false;
                end;
            end;
        elseif fruitType == FruitUtil.FRUITTYPE_UNKNOWN then
			self:stopAIThreshing();
            return;
        else
            -- turn
            local x,y,z = getWorldTranslation(self.aiTreshingDirectionNode);
            local lx,ly,lz = nil, nil, nil
            local rx,ry,rz = nil, nil, nil
			
            local dirX, dirZ = self.aiThreshingDirectionX, self.aiThreshingDirectionZ;
            local sideX, sideZ = -dirZ, dirX;
            local lInX,  lInY,  lInZ = getWorldTranslation(leftMarker);
            local rInX,  rInY,  rInZ = getWorldTranslation(rightMarker);
            local threshWidth = Utils.vector2Length(lInX-rInX, lInZ-rInZ);
			local leftFruit = 0
			local rightFruit = 0
            local turnLeft = true;
			local mythreshWidth = 0.51
			local mySideWatchDirOffset = -1.01
			local mySideWatchDirSize = 2.01
			local doBreak = false
			local leftSwathFound = false
			local rightSwathFound = false
			local leftSkipTurnStage2 = false
			local rightSkipTurnStage2 = false

			for transXDouble = 0,80 do
				local transX = transXDouble * 0.25
				for transZ = -6,14 do
					setTranslation(self.aiLeftCheck, transX*-1,-0.5,transZ)
					setTranslation(self.aiRightCheck, transX*1,-0.5,transZ)
					
					lx,ly,lz = getWorldTranslation(self.aiLeftCheck);
					rx,ry,rz = getWorldTranslation(self.aiRightCheck);
				
					lWidthX = lx - sideX*0.5*mythreshWidth + dirX * mySideWatchDirOffset
					lWidthZ = lz - sideZ*0.5*mythreshWidth + dirZ * mySideWatchDirOffset
					lStartX = lWidthX - sideX*0.7*mythreshWidth
					lStartZ = lWidthZ - sideZ*0.7*mythreshWidth
					lHeightX = lStartX + dirX* mySideWatchDirSize
					lHeightZ = lStartZ + dirZ* mySideWatchDirSize
					
					rWidthX = rx + sideX*0.5*mythreshWidth + dirX * mySideWatchDirOffset
					rWidthZ = rz + sideZ*0.5*mythreshWidth + dirZ * mySideWatchDirOffset
					rStartX = rWidthX + sideX*0.7*mythreshWidth
					rStartZ = rWidthZ + sideZ*0.7*mythreshWidth
					rHeightX = rStartX + dirX* mySideWatchDirSize
					rHeightZ = rStartZ + dirZ* mySideWatchDirSize
					
					checkLeftFruit = EasyFlow.getFruitWindrowArea(fruitType, lStartX, lStartZ, lWidthX, lWidthZ, lHeightX, lHeightZ);
					checkRightFruit = EasyFlow.getFruitWindrowArea(fruitType, rStartX, rStartZ, rWidthX, rWidthZ, rHeightX, rHeightZ);
					
					leftFruit = leftFruit + checkLeftFruit
					rightFruit = rightFruit + checkRightFruit
					
					if (checkLeftFruit > 1 and leftSwathFound == false) or (checkRightFruit > 1 and rightSwathFound == false) then
						if transZ < 6 then
							self.moveForwards = true
							doBreak = true
							if transX < 1 then 
								self.turnTimer = self.turnTimeout;
							end
						end
					
						if checkLeftFruit > 5 and leftSwathFound == false then
							-- swath fund on left side - place left marker
							leftSwathFound = true
							if transX < 1 then
								setTranslation(self.aiLeftCheck, (transX + threshWidth*0.9)*-1,-0.5,transZ)
							else
								if transX > 7.5 then
									leftSkipTurnStage2 = true
								end
								setTranslation(self.aiLeftCheck, (transX + threshWidth*0.7)*-1,-0.5,transZ)
							end
							Nlx,Nly,Nlz = getWorldTranslation(self.aiLeftCheck)
						end
						
						if checkRightFruit > 5 and rightSwathFound == false then
							-- swath found on right side - place right marker
							rightSwathFound = true
							if transX < 1 then
								setTranslation(self.aiRightCheck, (transX + threshWidth*0.9)*1,-0.5,transZ)
							else
								if transX > 7.5 then
									rightSkipTurnStage2 = true
								end
								setTranslation(self.aiRightCheck, (transX + threshWidth*0.7)*1,-0.5,transZ)
							end
							Nrx,Nry,Nrz = getWorldTranslation(self.aiRightCheck)
						end
					end
				end
				if doBreak == true then
					break
				end
			end
			
			if self.moveForwards == true then
				if self.turnTimer > 0 then
					self.turnStage = 0
					self:setAIImplementsMoveDown(true);
				else
					self.turnStage = -1
					self:setAIImplementsMoveDown(false);
				end
				-- do not turn jet
				local x,y,z = getWorldTranslation(self.aiTreshingDirectionNode);
				local dirX, dirZ = self.aiThreshingDirectionX, self.aiThreshingDirectionZ;
				-- just drive along direction
				local targetX, targetZ = self.aiThreshingTargetX, self.aiThreshingTargetZ;
				local dx, dz = x-targetX, z-targetZ;
				local dot = dx*dirX + dz*dirZ;

				local projTargetX = targetX +dirX*dot;
				local projTargetZ = targetZ +dirZ*dot;

				--print("old target: "..targetX.." ".. targetZ .. " distOnDir " .. dot.." proj: "..projTargetX.." "..projTargetZ);

				newTargetX = projTargetX+self.aiThreshingDirectionX*self.lookAheadDistance;
				newTargetY = y;
				newTargetZ = projTargetZ+self.aiThreshingDirectionZ*self.lookAheadDistance;
				--print(distOnDir.." target: "..newTargetX.." ".. newTargetZ);
			else
				if leftFruit > 0 or rightFruit > 0 then
					if leftFruit > rightFruit then
						self.skipTurnStage2 = leftSkipTurnStage2
						turnLeft = true;
					else
						self.skipTurnStage2 = rightSkipTurnStage2
						turnLeft = false;
					end
				else	
					self:stopAIThreshing();
					return;
				end;
				-- turn to where more fruit is to cut
				local targetX, targetZ = self.aiThreshingTargetX, self.aiThreshingTargetZ;
				--local dx, dz = x-targetX, z-targetZ;
				--local dot = dx*dirX + dz*dirZ;
				--local x, z = targetX + dirX*dot, targetZ + dirZ*dot;
				--threshWidth = threshWidth*self.aiTurnThreshWidthScale;
				local markerSideOffset;
				if turnLeft then
					markerSideOffset, _, _ = worldToLocal(self.aiTreshingDirectionNode, Nlx,Nly,Nlz);
				else
					markerSideOffset, _, _ = worldToLocal(self.aiTreshingDirectionNode, Nrx,Nry,Nrz);
				end
				
				local myAiTurnThreshWidthMaxDifference = 0.6
				local areaOverlap = math.min(threshWidth*(1-self.aiTurnThreshWidthScale), myAiTurnThreshWidthMaxDifference);
				if markerSideOffset > 0 then
					markerSideOffset = math.max(markerSideOffset - areaOverlap, 0.01);
				else
					markerSideOffset = math.min(markerSideOffset + areaOverlap, -0.01);
				end

				local x,z = Utils.projectOnLine(x, z, targetX, targetZ, dirX, dirZ)
				newTargetX = x-sideX*markerSideOffset;
				newTargetY = y;
				newTargetZ = z-sideZ*markerSideOffset;

				self.aiThreshingDirectionX = -dirX;
				self.aiThreshingDirectionZ = -dirZ;
				self.turnStage = 1;
				self.aiRescueTimer = self.aiRescueTimeout;
				self.turnStageTimer = self.turnStage1Timeout;

				self.aiThreshingTargetBeforeTurnX = self.aiThreshingTargetX;
				self.aiThreshingTargetBeforeTurnZ = self.aiThreshingTargetZ;

				self.waitForTurnTime = self.time + self.waitForTurnTimeout;
				self:setAIImplementsMoveDown(false);
				-- do not thresh while turning
				self.allowsThreshing = false;
				updateWheels = false
				if turnLeft then
					--print("turning left ", threshWidth);
				else
					--print("turning right ", threshWidth);
				end;
			end
        end;
    else
        local x,y,z = getWorldTranslation(self.aiTreshingDirectionNode);
        local dirX, dirZ = self.aiThreshingDirectionX, self.aiThreshingDirectionZ;
        -- just drive along direction
        local targetX, targetZ = self.aiThreshingTargetX, self.aiThreshingTargetZ;
        local dx, dz = x-targetX, z-targetZ;
        local dot = dx*dirX + dz*dirZ;

        local projTargetX = targetX +dirX*dot;
        local projTargetZ = targetZ +dirZ*dot;

        --print("old target: "..targetX.." ".. targetZ .. " distOnDir " .. dot.." proj: "..projTargetX.." "..projTargetZ);

        newTargetX = projTargetX+self.aiThreshingDirectionX*self.lookAheadDistance;
        newTargetY = y;
        newTargetZ = projTargetZ+self.aiThreshingDirectionZ*self.lookAheadDistance;
        --print(distOnDir.." target: "..newTargetX.." ".. newTargetZ);
    end;

    if updateWheels then
        local lx, lz = AIVehicleUtil.getDriveDirection(self.aiTreshingDirectionNode, newTargetX, newTargetY, newTargetZ);

        if self.turnStage == 2 and math.abs(lx) < 0.1 then
            AICombine.switchToTurnStage3(self);
            moveForwards = true;
        end;

        AIVehicleUtil.driveInDirection(self, dt, 25, 0.5, 0.5, 20, true, moveForwards, lx, lz, speedLevel, 0.9);

        --local maxAngle = 0.785398163; --45?;
        local maxlx = 0.7071067; --math.sin(maxAngle);
        local colDirX = lx;
        local colDirZ = lz;

        if colDirX > maxlx then
            colDirX = maxlx;
            colDirZ = 0.7071067; --math.cos(maxAngle);
        elseif colDirX < -maxlx then
            colDirX = -maxlx;
            colDirZ = 0.7071067; --math.cos(maxAngle);
        end;

        for triggerId,_ in pairs(self.numCollidingVehicles) do
            AIVehicleUtil.setCollisionDirection(self.aiTreshingDirectionNode, triggerId, colDirX, colDirZ);
        end;
    end;

    self.aiThreshingTargetX = newTargetX;
    self.aiThreshingTargetZ = newTargetZ;
end;

function EasyFlow.getFruitWindrowArea(fruitId, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)
    local ids = g_currentMission.fruits[fruitId];
    if ids == nil or ids.windrowId == 0 then
        return 0, 0;
    end
    local id = ids.windrowId;
    local x,z, widthX,widthZ, heightX,heightZ = Utils.getXZWidthAndHeight(id, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ);
    local ret, _, total = getDensityParallelogram(id, x, z, widthX, widthZ, heightX, heightZ, 0, g_currentMission.numWindrowChannels);
    return ret, total;
end

function EasyFlow:draw()
	local combineAI = self:getCombine();

	if combineAI ~= nil then		
		if not combineAI.isHired then
			if self.grassForceGrass == true then
				g_currentMission:addHelpButtonText(g_i18n:getText("grassForceGrass"), InputBinding.IMPLEMENT_EXTRA4);
			else
				g_currentMission:addHelpButtonText(g_i18n:getText("grassForceChaff"), InputBinding.IMPLEMENT_EXTRA4);
			end
		end;
	end
end;

function EasyFlow:onAttach(attacherVehicle)
	-- add some fruits from cutter to combine
    for fruitType,_ in pairs(self.combineGrainTankFruitTypes) do
		if not attacherVehicle.grainTankFruitTypes[fruitType] then
			attacherVehicle.pipeParticleSystems[fruitType] = attacherVehicle.defaultPipeParticleSystem
			attacherVehicle.grainTankFruitTypes[fruitType] = true;
			self.combineGrainTankFruitTypesAdd[fruitType] = true
		end
	end
end;

function EasyFlow:onDetach(attacherVehicle)
	-- remove added fruits from combine
    for fruitType,_ in pairs(self.combineGrainTankFruitTypes) do
		if attacherVehicle.grainTankFruitTypes[fruitType] and self.combineGrainTankFruitTypesAdd[fruitType] then
			attacherVehicle.pipeParticleSystems[fruitType] = nil
			attacherVehicle.grainTankFruitTypes[fruitType] = nil
			self.combineGrainTankFruitTypesAdd[fruitType] = nil
		end
	end
	
    if self.deactivateOnDetach then
        EasyFlow.onDeactivate(self);
    end;
end;

function EasyFlow:onLeave()
    if self.deactivateOnLeave then
        EasyFlow.onDeactivate(self);
    end;
end;

function EasyFlow:onDeactivate()
    self:onStopReel();
    Utils.setEmittingState(self.threshingParticleSystems, false);
	
	for _, particleSystem in pairs(self.particleSystems) do
		Utils.setEmittingState(particleSystem, false);
	end
	
    self.speedViolationTimer = self.speedViolationMaxTime;
end;

function EasyFlow:setReelSpeedSpace(speedScale)
    self.reelSpeedScale = speedScale;
end;

function EasyFlow:onStartReel()
    self.reelStarted = true;

    for _, part in pairs(self.cutterThreshingUVScrollParts) do
        setShaderParameter(part.node, "uvScrollSpeed", part.speed[1], part.speed[2], 0, 0, false);
    end;

    if self.cutterStartAnimation ~= nil and self.playAnimation ~= nil then
        self:playAnimation(self.cutterStartAnimation, self.cutterStartAnimationSpeedScale, nil, true);
    end
end;

function EasyFlow:onStopReel()
    self.reelStarted = false;
    Utils.setEmittingState(self.threshingParticleSystems, false);
	
	for _, particleSystem in pairs(self.particleSystems) do
		Utils.setEmittingState(particleSystem, false);
	end
	
    self.speedViolationTimer = self.speedViolationMaxTime;

    for _, part in pairs(self.cutterThreshingUVScrollParts) do
        setShaderParameter(part.node, "uvScrollSpeed", 0, 0, 0, 0, false);
    end;

    if self.cutterStartAnimation ~= nil and self.playAnimation ~= nil then
        self:playAnimation(self.cutterStartAnimation, -self.cutterStartAnimationSpeedScale, nil, true);
    end
end;

function EasyFlow:getDirectionSnapAngle(superFunc)
    local snapAngle = 0;
    for fruitType,_ in pairs(self.fruitTypes) do
        local desc = FruitUtil.fruitIndexToDesc[fruitType];
        if desc ~= nil then
            snapAngle = math.max(snapAngle, desc.directionSnapAngle);
        end
    end
    return math.max(snapAngle, superFunc(self));
end

function EasyFlow:getCombine()
    if self.getIsThreshingAllowed ~= nil and self.getIsCutterFruitTypeAllowed ~= nil and self.grainTankFillLevel ~= nil then
        return self;
    else
        local c = self.attacherVehicle;
        if c ~= nil and c.getIsThreshingAllowed ~= nil and c.getIsCutterFruitTypeAllowed ~= nil and c.grainTankFillLevel ~= nil then
            return c;
        end
    end;
    return nil;
end

--[[function EasyFlow:resetFruitType()
    self.currentFruitType = FruitUtil.FRUITTYPE_UNKNOWN;
    self.currentInputFruitType = FruitUtil.FRUITTYPE_UNKNOWN;
    self.lastCutterArea = 0;
end;]]

--[[function EasyFlow:setFruitType(fruitType)
    if self.currentInputFruitType ~= fruitType then
        self.currentInputFruitType = fruitType;
        self.currentFruitType = fruitType;
        if self.convertedFruits[fruitType] ~= nil then
            self.currentFruitType = self.convertedFruits[fruitType];
        end;
        self.lastCutterArea = 0;

        EasyFlow.updateExtraObjects(self)
    end;
end;]]

function EasyFlow.getUseLowSpeedLimit(self)
    local combineSize = self.combineSize;
    if combineSize == nil then
        if self.attacherVehicle ~= nil then
            combineSize = self.attacherVehicle.combineSize;
        end
    end

    if self.forceLowSpeed or (combineSize ~= nil and self.preferedCombineSize > combineSize) then
        return true;
    end
    return false;
end;

function EasyFlow.updateExtraObjects(self)
    if self.currentExtraObject ~= nil then
        setVisibility(self.currentExtraObject, false);
        self.currentExtraObject = nil;
    end;
    if self.currentInputFruitType ~= FruitUtil.FRUITTYPE_UNKNOWN then
        local name = FruitUtil.fruitIndexToDesc[self.currentInputFruitType].name;
        local extraObject = self.fruitExtraObjects[name];
        if extraObject ~= nil then
            setVisibility(extraObject, true);
            self.currentExtraObject = extraObject;
        end;
    end;
end;

function EasyFlow:changeGrassForceGrass(grassForceGrass, noEventSend)
    if grassForceGrass ~= self.grassForceGrass then
        if noEventSend == nil or noEventSend == false then
            if g_server ~= nil then
                g_server:broadcastEvent(EasyFlowGrassForceGrassEvent:new(self, grassForceGrass), nil, nil, self);
            else
                g_client:getServerConnection():sendEvent(EasyFlowGrassForceGrassEvent:new(self, grassForceGrass));
            end;
        end;
        local inputType = "grass"
        local outputType = "chaff"
		if grassForceGrass == true then
			inputType = "grass"
			outputType = "grass_windrow"
		end

		local inputDesc = Fillable.fillTypeNameToInt[inputType];
		local outputDesc = Fillable.fillTypeNameToInt[outputType];
		if inputDesc ~= nil and outputDesc ~= nil then
			self.convertedFruits[inputDesc] = outputDesc;
		end;
		self.grassForceGrass = grassForceGrass
    end;
end;

function EasyFlow.findAutoAimTrailerToUnload(self, fruitType)
    local trailer = nil;
    local smallestTrailerId = nil;
    local trailerIsAttached = false;
    for trailerInRange, pipeStage in pairs(self.combineTrailersInRange) do
        if trailerInRange.isDeleted then
            self.combineTrailersInRange[trailerInRange] = nil;
        else
            if trailerInRange:allowFillType(fruitType) and trailerInRange.getAllowFillFromAir ~= nil and trailerInRange:getAllowFillFromAir() and trailerInRange.fillLevel < trailerInRange.capacity then
                local id = networkGetObjectId(trailerInRange);
                -- always take the trailer with the smallest network id which is attached if possible
                -- This is deterministic and is the same on the client
                local isAttached = trailerInRange:getIsAttachedTo(self)
                if trailer == nil or (isAttached and not trailerIsAttached) or (isAttached == trailerIsAttached and id < smallestTrailerId) then
                    trailer = trailerInRange;
                    smallestTrailerId = id;
                    trailerIsAttached = isAttached
                end;
            end;
        end
    end;
    return trailer;
end;

function EasyFlow.findTrailerToUnload(self, fruitType)
    local x,y,z = getWorldTranslation(self.pipeRaycastNode);
    local dx,dy,dz = localDirectionToWorld(self.pipeRaycastNode, 0,-1,0);

    self.trailerFound = 0;
    self.trailerFoundDistance = 0;
    raycastAll(x, y, z, dx,dy,dz, "findTrailerRaycastCallback", self.pipeRaycastDistance - 1, self);
	
    local trailer = g_currentMission.nodeToVehicle[self.trailerFound];
	
    if trailer == nil or not trailer:allowFillType(fruitType) or trailer.getAllowFillFromAir == nil or not trailer:getAllowFillFromAir() or trailer.fillLevel >= trailer.capacity then
        return nil;
    end;
    return trailer, self.trailerFoundDistance;
end;