--[[
	MixerWagon
	Specialization to allow differend output fill type for mixer wagons and set an mixing time! This should already done by GIANTS Software...
	
	@author:  	Ifko[nator]
	@date:  	05.06.2019
    @version:	1.1
    
    @history:   v1.0 @ 21.05.2019 - initial release in FS 19
                v1.1 @ 05.06.2019 - add failedOutputFillType if it is not added. Needed for switching the fillType after mixing. (VertiMix ..)
                                  - added possibility to show the hud for mixing ratio if you are outsite and near of the mixer. On foot or in a vehicle.
]]

MixerWagon = {};
MixerWagon.currentModDirectory = g_currentModDirectory;
MixerWagon.currentModName = g_currentModName;
MixerWagon.distanceToDrawMixingRatio = 8;

source("dataS/scripts/gui/hud/MixerWagonHUDExtension.lua");
VehicleHUDExtension.registerHUDExtension(MixerWagon, MixerWagonHUDExtension);

function MixerWagon.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(Trailer, specializations) and SpecializationUtil.hasSpecialization(TurnOnVehicle, specializations);
end;

function MixerWagon.registerEventListeners(vehicleType)
	local functionNames = {
        "onLoad",
        "saveToXMLFile",
		"onDelete",
		"onReadStream",
		"onWriteStream",
		"onReadUpdateStream",
		"onWriteUpdateStream",
        "onUpdate",
		"onTurnedOn",
		"onTurnedOff",
		"onFillUnitFillLevelChanged"
	};

	for _, functionName in ipairs(functionNames) do
		SpecializationUtil.registerEventListener(vehicleType, functionName, MixerWagon);
	end;
end;

function MixerWagon.registerFunctions(vehicleType)
	local functionNames = {
        "mixerWagonBaleTriggerCallback",
        "calculatePercentage",
        "resetMixingTime",
        "setMixingTime"
	};
	
	for _, functionName in ipairs(functionNames) do
		SpecializationUtil.registerFunction(vehicleType, functionName, MixerWagon[functionName]);
	end;
end;


function MixerWagon.registerOverwrittenFunctions(vehicleType)
	local overwrittenFunctions = {
		"addFillUnitFillLevel",
		"getFillUnitAllowsFillType",
        "getDischargeFillType"
	};

	for _, overwrittenFunction in ipairs(overwrittenFunctions) do
		SpecializationUtil.registerOverwrittenFunction(vehicleType, overwrittenFunction, MixerWagon[overwrittenFunction]);
	end;
end;

function MixerWagon:onLoad(savegame) 
    MixerWagon.distanceToDrawMixingRatio = Utils.getNoNil(getXMLInt(self.xmlFile, "vehicle.mixerWagon#distanceToDrawMixingRatio"), MixerWagon.distanceToDrawMixingRatio); --## in meter!
    
    self.spec_mixerWagon = {};

    local specMixerWagon = self.spec_mixerWagon;
    
    specMixerWagon.needTurnOn = Utils.getNoNil(getXMLBool(self.xmlFile, "vehicle.mixerWagon#needTurnOn"), true);
    specMixerWagon.showMixerWagonHud = Utils.getNoNil(getXMLBool(self.xmlFile, "vehicle.mixerWagon#showHud"), true);
    specMixerWagon.headerText = Utils.getNoNil(getXMLString(self.xmlFile, "vehicle.mixerWagon#headerText"), "info_mixingRatio"); 
    specMixerWagon.timeIconFilename = Utils.getFilename(Utils.getNoNil(getXMLString(self.xmlFile, "vehicle.mixerWagon#timeIconFilename"), ""), MixerWagon.currentModDirectory); 
    specMixerWagon.outputFillType = FillType[string.upper(Utils.getNoNil(getXMLString(self.xmlFile, "vehicle.mixerWagon#outputFillType"), "FORAGE"))];
    specMixerWagon.failedOutputFillType = FillType[string.upper(Utils.getNoNil(getXMLString(self.xmlFile, "vehicle.mixerWagon#failedOutputFillType"), "FORAGE_MIXING"))];
    specMixerWagon.requiredMixingTime = Utils.getNoNil(getXMLInt(self.xmlFile, "vehicle.mixerWagon#mixingTime"), 60) * g_currentMission.missionInfo.difficulty; --## in seconds, 0 = NO MIXING TIME!
    specMixerWagon.currentMixingTime = 0;
    specMixerWagon.mixingTimePercentage = 0;
    specMixerWagon.isFirstRun = true;

    MixerWagonHUDExtension.canDraw = Utils.overwrittenFunction(MixerWagonHUDExtension.canDraw, MixerWagon.canDraw);
    MixerWagonHUDExtension.draw = Utils.overwrittenFunction(MixerWagonHUDExtension.draw, MixerWagon.drawStatusBarExtension);
    
    InputHelpDisplay.refreshHUDExtensions = Utils.overwrittenFunction(InputHelpDisplay.refreshHUDExtensions, MixerWagon.refreshHUDExtensions);

    if self.isClient then
        specMixerWagon.mixAnimationNodes = g_animationManager:loadAnimations(self.xmlFile, "vehicle.mixerWagon.mixAnimationNodes", self.components, self, self.i3dMappings);
        specMixerWagon.pickupAnimationNodes = g_animationManager:loadAnimations(self.xmlFile, "vehicle.mixerWagon.pickupAnimationNodes", self.components, self, self.i3dMappings);
    end;

    if self.isServer then
        specMixerWagon.baleTriggerNode = I3DUtil.indexToObject(self.components, getXMLString(self.xmlFile, "vehicle.mixerWagon.baleTrigger#node"), self.i3dMappings);
        
        if specMixerWagon.baleTriggerNode ~= nil then
            addTrigger(specMixerWagon.baleTriggerNode, "mixerWagonBaleTriggerCallback", self);
        end;
    end;
    
    specMixerWagon.fillUnitIndex = Utils.getNoNil(getXMLInt(self.xmlFile, "vehicle.mixerWagon#fillUnitIndex"), 1);

    --## disable sync and saving of fillLevels for fillUnit - will be handled by mixerWagon
    local fillUnit = self:getFillUnitByIndex(specMixerWagon.fillUnitIndex);
    
    if fillUnit ~= nil then
        fillUnit.needsSaving = false; 
        fillUnit.synchronizeFillLevel = false;
    end;

    specMixerWagon.mixerWagonFillTypes = {};
    specMixerWagon.fillTypeToMixerWagonFillType = {};

    local sumRatio = 0;
    local mixerWagonFillTypeNumber = 0;

    while true do
        local baseName = string.format("vehicle.mixerWagon.mixerWagonFillTypes.mixerWagonFillType(%d)", mixerWagonFillTypeNumber);

        if not hasXMLProperty(self.xmlFile, baseName) then
            break;
        end;

        local fillTypeNumber = 0;
        local entry = {};

        entry.fillTypes = {};

        while true do
            local fillTypeKey = baseName..string.format(".fillType(%d)", fillTypeNumber);

            if not hasXMLProperty(self.xmlFile, fillTypeKey) then
                local failedOutputFillType = g_fillTypeManager:getFillTypeIndexByName(string.lower(getXMLString(self.xmlFile, "vehicle.mixerWagon#failedOutputFillType")));
                
                if specMixerWagon.fillTypeToMixerWagonFillType[failedOutputFillType] == nil then
                    --## add failedOutputFillType if it is not added. Needed for switching the fillType after mixing.

                    entry.fillTypes[failedOutputFillType] = true;

                    specMixerWagon.fillTypeToMixerWagonFillType[failedOutputFillType] = entry;
                end;

                break;
            end;

            local fillTypeStr = getXMLString(self.xmlFile, fillTypeKey .. "#fillType");

            if fillTypeStr ~= nil then
                local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(fillTypeStr);
                
                if fillTypeIndex ~= nil then
                    if specMixerWagon.fillTypeToMixerWagonFillType[fillTypeIndex] == nil then
                        entry.fillTypes[fillTypeIndex] = true;

                        specMixerWagon.fillTypeToMixerWagonFillType[fillTypeIndex] = entry;
                    else
                        g_logManager:xmlWarning(self.configFileName, "MixerWagonFillType '%s' in '%s' already used! Ignoring it!", fillTypeKey, fillTypeStr);
                    end;
                else
                    g_logManager:xmlWarning(self.configFileName, "FillType '%s' not defined for mixerWagonFillType '%s'!", fillTypeStr, fillTypeKey);
                end;
            end;

            fillTypeNumber = fillTypeNumber + 1;
        end;

        entry.name = Utils.getNoNil(getXMLString(self.xmlFile, baseName.."#name"), "unknown");
        entry.minPercentage = Utils.getNoNil(getXMLFloat(self.xmlFile, baseName.."#minPercentage"), 0) * 0.01;
        entry.maxPercentage = Utils.getNoNil(getXMLFloat(self.xmlFile, baseName.."#maxPercentage"), 100) * 0.01;
        entry.ratio = entry.maxPercentage - entry.minPercentage;
        entry.fillLevel = 0;

        if next(entry.fillTypes) ~= nil then
            sumRatio = sumRatio + entry.ratio;
        end;
        
        table.insert(specMixerWagon.mixerWagonFillTypes, entry);
        
        mixerWagonFillTypeNumber = mixerWagonFillTypeNumber + 1;
    end;

    for _, entry in ipairs(specMixerWagon.mixerWagonFillTypes) do
        entry.ratio = entry.ratio / sumRatio;
    end;

    specMixerWagon.dirtyFlag = self:getNextDirtyFlag();

    local uiScale = g_gameSettings:getValue("uiScale");
    local width, height = getNormalizedScreenValues(208 * uiScale, 12 * uiScale);
    
    specMixerWagon.statusBarColorGood = MixerWagonHUDExtension.COLOR.STATUS_BAR_GOOD;
    specMixerWagon.statusBarColorAlmostGood = {1, 1, 0, 1};
    specMixerWagon.statusBarColorBetter = {1, 0.2, 0, 1};
    specMixerWagon.statusBarColorBad = MixerWagonHUDExtension.COLOR.STATUS_BAR_BAD;
    specMixerWagon.statusBar = StatusBar:new(g_baseUIFilename, g_colorBgUVs, nil, MixerWagonHUDExtension.COLOR.STATUS_BAR_BG, specMixerWagon.statusBarColorBad, nil, 0, 0, width, height);
    
    if specMixerWagon.timeIconFilename ~= "" then
        width, height = getNormalizedScreenValues(20 * uiScale, 20 * uiScale)

        specMixerWagon.timeOverlay = Overlay:new(specMixerWagon.timeIconFilename, 0, 0, width, height);
    end;
    
    if savegame ~= nil then
        local currentMixingTime = getXMLInt(savegame.xmlFile, savegame.key .. "." .. MixerWagon.currentModName .. ".mixerWagonImproved#currentMixingTime");
        
        specMixerWagon.forceUpdate = currentMixingTime ~= nil;

        if currentMixingTime ~= nil then
            specMixerWagon.currentMixingTime = currentMixingTime;
        end;
        
        if specMixerWagon.requiredMixingTime > 0 then
            self:calculatePercentage();
        end;
        
        for number, entry in ipairs(specMixerWagon.mixerWagonFillTypes) do
            local fillTypeKey = savegame.key .. string.format("." .. MixerWagon.currentModName .. ".mixerWagonImproved.fillType(%d)#fillLevel", number - 1);
            local fillLevel = Utils.getNoNil(getXMLFloat(savegame.xmlFile, fillTypeKey), 0);

            if fillLevel > 0 then
                self:addFillUnitFillLevel(self:getOwnerFarmId(), specMixerWagon.fillUnitIndex, fillLevel, next(entry.fillTypes), ToolType.UNDEFINED, nil);
            end;
        end;
    end;

    local specMotorized = self.spec_motorized;

    if specMotorized ~= nil then
        specMotorized.motorStopTimerDurationBackUp = specMotorized.motorStopTimerDuration;
        specMotorized.motorStopTimerDuration = math.huge;
    end;
end;

function MixerWagon:refreshHUDExtensions()
    local uiScale = self:getScale();

    for vehicle, specs in pairs(self.vehicleSpecializationsBuffer) do
        local shouldDeleteVehicleHUDExtension = g_currentMission.controlledVehicle ~= nil 
            and g_currentMission.controlledVehicle.spec_enterable ~= nil 
            and g_currentMission.controlledVehicle.spec_enterable.isEntered
            and vehicle.spec_mixerWagon ~= nil
        ;

        if shouldDeleteVehicleHUDExtension then
            for k, hudExtension in pairs(self.vehicleHudExtensions) do
                hudExtension:delete();

                self.vehicleHudExtensions[k] = nil;
            end;
        end;
        
        for _, spec in pairs(specs) do
            local hudExtension = self.vehicleHudExtensions[spec];

            if hudExtension == nil and VehicleHUDExtension.hasHUDExtensionForSpecialization(spec) then
                hudExtension = VehicleHUDExtension.createHUDExtensionForSpecialization(spec, vehicle, uiScale, InputHelpDisplay.COLOR.HELP_TEXT, self.helpTextSize);

                self.vehicleHudExtensions[spec] = hudExtension;
            end;
        end;
    end;
end;

function MixerWagon:saveToXMLFile(xmlFile, key, usedModNames)
    local specMixerWagon = self.spec_mixerWagon;
    
    for i, fillType in ipairs(specMixerWagon.mixerWagonFillTypes) do
        local fillTypeKey = string.format("%s.fillType(%d)", key, i - 1);

        setXMLFloat(xmlFile, fillTypeKey .. "#fillLevel", fillType.fillLevel);
    end;

    setXMLInt(xmlFile, key .. "#currentMixingTime", specMixerWagon.currentMixingTime);
end;

function MixerWagon:drawStatusBarExtension(superFunc, leftPosX, rightPosX, posY)    
    if not self:canDraw() then
        return;
    end;
    
    local specMixerWagon = self.mixerWagon;
    local hasUpdated = false;

    for _, vehicle in pairs(g_currentMission.vehicles) do
		if vehicle.spec_mixerWagon ~= nil then
			local distanceToVehicle = calcDistanceFrom(vehicle.rootNode, getCamera());
			
			if g_currentMission.controlledVehicle ~= nil then
				distanceToVehicle = calcDistanceFrom(vehicle.rootNode, g_currentMission.controlledVehicle.rootNode);
            end;
    
            if distanceToVehicle < MixerWagon.distanceToDrawMixingRatio then
                specMixerWagon = vehicle.spec_mixerWagon;
                self.vehicle = vehicle;

                if not hasUpdated then
                    self.fillTypeStatus = {}; --## delelte the fill type display entries

                    for _, mixerWagonFillType in ipairs(specMixerWagon.mixerWagonFillTypes) do
                        local fillType = g_fillTypeManager:getFillTypeByName(mixerWagonFillType.name);

                        if fillType ~= nil then
                            local entry = {fillType = fillType, overlay = nil, statusBar = nil, statusBarColor = nil, statusBarColor2 = nil, minPercentage = 0, maxPercentage = 0, fillLevel = 0};

                            entry.minPercentage = mixerWagonFillType.minPercentage;
                            entry.maxPercentage = mixerWagonFillType.maxPercentage;

                            local uiScale = g_gameSettings:getValue("uiScale");
                            local width, height = getNormalizedScreenValues(20 * uiScale, 20 * uiScale);

                            entry.overlay = Overlay:new(fillType.hudOverlayFilenameSmall, 0, 0, width, height);
                            entry.overlay:setColor(unpack(self.uiTextColor));
                        
                            entry.statusBarColor = MixerWagonHUDExtension.COLOR.STATUS_BAR_GOOD;
                            entry.statusBarColor2 = MixerWagonHUDExtension.COLOR.STATUS_BAR_BAD;

                            local width, height = getNormalizedScreenValues(208 * uiScale, 12 * uiScale);

                            entry.statusBar = StatusBar:new(g_baseUIFilename, g_colorBgUVs, nil, MixerWagonHUDExtension.COLOR.STATUS_BAR_BG, entry.statusBarColor2, nil, 0, 0, width, height);

                            table.insert(self.fillTypeStatus, entry);
                        end;
                    end;

                    hasUpdated = true;
                end;
            end;
        end;
    end;

    local headerText = Utils.getNoNil(specMixerWagon.headerText, "info_mixingRatio");

	if headerText:sub(1, 6) == "$l10n_" then
		headerText = g_i18n:getText(headerText:sub(7));
	elseif g_i18n:hasText(headerText) then
		headerText = g_i18n:getText(headerText);
	end;

    setTextColor(unpack(self.uiTextColor));
    setTextBold(true);
    setTextAlignment(RenderText.ALIGN_LEFT);

    renderText(leftPosX, posY + (self.helpHeightPerFruit - self.uiTextSize) * 0.5, self.uiTextSize, headerText);
    
    setTextBold(false);

    local fillLevel = self.vehicle:getFillUnitFillLevel(specMixerWagon.fillUnitIndex);
    local totalFillLevel = 0;
    
    if fillLevel > 0 then
        for i, mixerWagonFillType in ipairs(specMixerWagon.mixerWagonFillTypes) do
            totalFillLevel = totalFillLevel + mixerWagonFillType.fillLevel;
            --## fill type display status is a synchronous array -> set corresponding fill level info

            self.fillTypeStatus[i].fillLevel = mixerWagonFillType.fillLevel;
        end;
    end;

    for _, fillTypeDisplay in ipairs(self.fillTypeStatus) do
        posY = posY - self.helpHeightPerFruit;

        local percentage = 0;

        if fillLevel > 0 then
            percentage = fillTypeDisplay.fillLevel / totalFillLevel;
        end;

        if fillTypeDisplay.fillLevel > 0 then
            if self.vehicle:getFillUnitFillType(specMixerWagon.fillUnitIndex) ~= Utils.getNoNil(specMixerWagon.failedOutputFillType, FillType.FORAGE_MIXING)
                or (percentage >= fillTypeDisplay.minPercentage and percentage <= fillTypeDisplay.maxPercentage) 
            then
                fillTypeDisplay.statusBar:setColor(unpack(Utils.getNoNil(specMixerWagon.statusBarColorGood, MixerWagonHUDExtension.COLOR.STATUS_BAR_GOOD)));
            else
                fillTypeDisplay.statusBar:setColor(unpack(Utils.getNoNil(specMixerWagon.statusBarColorBad, MixerWagonHUDExtension.COLOR.STATUS_BAR_BAD)));
            end;
        end;

        fillTypeDisplay.statusBar:setPosition(rightPosX - fillTypeDisplay.statusBar.width, posY + (self.helpHeightPerFruit - fillTypeDisplay.statusBar.height) * 0.5);
        fillTypeDisplay.statusBar:setValue(percentage);
        fillTypeDisplay.statusBar:render();

        if fillTypeDisplay.overlay ~= nil then
            fillTypeDisplay.overlay:setPosition(leftPosX, posY + (self.helpHeightPerFruit - fillTypeDisplay.overlay.height) * 0.5);
            fillTypeDisplay.overlay:render();
        end;

        renderText(leftPosX + self.fillLevelTextOffsetX, posY + self.fillLevelTextOffsetY, self.uiTextSize, string.format("%1.2f%%", percentage * 100));

        local y = fillTypeDisplay.statusBar.y + fillTypeDisplay.statusBar.height * 0.5;
        local maxLeft = fillTypeDisplay.statusBar.x + fillTypeDisplay.statusBar.width;
        local xLeft =  math.min(fillTypeDisplay.statusBar.x + fillTypeDisplay.statusBar.width * fillTypeDisplay.minPercentage, maxLeft);
        local xRight = math.min(fillTypeDisplay.statusBar.x + fillTypeDisplay.statusBar.width * fillTypeDisplay.maxPercentage, maxLeft);

        self.fillRangeMarkerOverlay:setInvertX(false);
        self.fillRangeMarkerOverlay:setPosition(xLeft, y);
        self.fillRangeMarkerOverlay:render();
        self.fillRangeMarkerOverlay:setInvertX(true); --## base sprite points right, need to point to the left now
        self.fillRangeMarkerOverlay:setPosition(xRight - self.fillRangeMarkerOverlay.width, y);
        self.fillRangeMarkerOverlay:render();
    end;
    
    posY = posY - self.helpHeightPerFruit;
   
    if Utils.getNoNil(specMixerWagon.requiredMixingTime, 0) > 0 then
        setTextBold(true);
        
        renderText(leftPosX, posY - (self.helpHeightPerFruit - self.uiTextSize) * 0.5, self.uiTextSize, g_i18n:getText("NEEDED_MIXING_TIME"));

        setTextBold(false);

        local neededTime = MathUtil.clamp(math.ceil(specMixerWagon.requiredMixingTime - (specMixerWagon.currentMixingTime / 1000)), 0, specMixerWagon.requiredMixingTime);

        local function formatTime(time)
            local function getExtraZero(time)    
                if time < 10 then
                    time = "0" .. time;
                end;

                return time;
            end;

            local hours = getExtraZero(math.floor(neededTime / 3600));
            local minutes = getExtraZero(math.floor((neededTime % 3600) / 60));
            local seconds = getExtraZero(math.floor(neededTime % 60));
        
            return hours .. "h:" .. minutes .. "m:" .. seconds .. "s";
        end;

        renderText(leftPosX + self.fillLevelTextOffsetX, posY - (self.helpHeightPerFruit - self.uiTextSize) * 2.4, self.uiTextSize, formatTime(neededTime));

        if specMixerWagon.timeOverlay ~= nil then
            specMixerWagon.timeOverlay:setPosition(leftPosX, posY - (self.helpHeightPerFruit - self.uiTextSize) * 2.6);
            specMixerWagon.timeOverlay:render();
        end;

        local mixingTimePercentage = specMixerWagon.mixingTimePercentage;

        if fillLevel == 0 then
            mixingTimePercentage = 0;

            if specMixerWagon.currentMixingTime > 0 and self.resetMixingTime ~= nil then
                self:resetMixingTime();
            end;
        end;

        if mixingTimePercentage == 1 then
            specMixerWagon.statusBar:setColor(unpack(specMixerWagon.statusBarColorGood));
        elseif mixingTimePercentage > 0.7 and mixingTimePercentage < 1 then
            specMixerWagon.statusBar:setColor(unpack(specMixerWagon.statusBarColorAlmostGood));
        elseif mixingTimePercentage > 0.35 and mixingTimePercentage < 0.7 then
            specMixerWagon.statusBar:setColor(unpack(specMixerWagon.statusBarColorBetter));
        else
            specMixerWagon.statusBar:setColor(unpack(specMixerWagon.statusBarColorBad));
        end;

        specMixerWagon.statusBar:setPosition(rightPosX - specMixerWagon.statusBar.width, posY - (self.helpHeightPerFruit - specMixerWagon.statusBar.height) * 1.8);
        specMixerWagon.statusBar:setValue(mixingTimePercentage);
        specMixerWagon.statusBar:render();

        self.displayHeight = (#specMixerWagon.mixerWagonFillTypes + 3.5) * self.helpHeightPerFruit + self.helpHeightOffset; --## extend help window for mixing time satus bar
    else
        self.displayHeight = (#specMixerWagon.mixerWagonFillTypes + 1) * self.helpHeightPerFruit + self.helpHeightOffset; --## restore help window size for mixing wagons without time satus bar
    end;
   
    return posY;
end;

function MixerWagon:canDraw()
    local specMixerWagon = self.mixerWagon;
    local getIsActiveForInput = self.vehicle:getIsActiveForInput(true);
    local isInRange = false;
    
    for _, vehicle in pairs(g_currentMission.vehicles) do
		if vehicle.spec_mixerWagon ~= nil then
			local distanceToVehicle = calcDistanceFrom(vehicle.rootNode, getCamera());
			
			if g_currentMission.controlledVehicle ~= nil then
				distanceToVehicle = calcDistanceFrom(vehicle.rootNode, g_currentMission.controlledVehicle.rootNode);
            end;
            
            if distanceToVehicle < MixerWagon.distanceToDrawMixingRatio then
                isInRange = true;
                
                specMixerWagon = vehicle.spec_mixerWagon;
            end;
        end;
    end;
    
    return Utils.getNoNil(specMixerWagon.showMixerWagonHud, getIsActiveForInput) and getIsActiveForInput or isInRange;
end;

function MixerWagon:calculatePercentage()
    local specMixerWagon = self.spec_mixerWagon;
    
    specMixerWagon.mixingTimePercentage = specMixerWagon.currentMixingTime / (specMixerWagon.requiredMixingTime * 1000);
end;

function MixerWagon:resetMixingTime()
    self:setMixingTime(0);
    self:calculatePercentage();
end;

function MixerWagon:setMixingTime(currentMixingTime)
    local specMixerWagon = self.spec_mixerWagon;
    
    if currentMixingTime ~= specMixerWagon.currentMixingTime then
       specMixerWagon.currentMixingTime = currentMixingTime;
    end;
end;

function MixerWagon:onDelete()
    local specMixerWagon = self.spec_mixerWagon;

    if self.isServer then
        if specMixerWagon.baleTriggerNode ~= nil then
            removeTrigger(specMixerWagon.baleTriggerNode);
        end;
    end;

    if self.isClient then
        g_animationManager:deleteAnimations(specMixerWagon.mixAnimationNodes);
        g_animationManager:deleteAnimations(specMixerWagon.pickupAnimationNodes);
    end;
end;

function MixerWagon:onReadStream(streamId, connection)
    local specMixerWagon = self.spec_mixerWagon;

    for _, entry in ipairs(specMixerWagon.mixerWagonFillTypes) do
        local fillLevel = streamReadFloat32(streamId);

        if fillLevel > 0 then
            self:addFillUnitFillLevel(self:getOwnerFarmId(), specMixerWagon.fillUnitIndex, fillLevel, next(entry.fillTypes), ToolType.UNDEFINED, nil);
        end;
    end;

    specMixerWagon.currentMixingTime = streamReadInt32(streamId);
end;

function MixerWagon:onWriteStream(streamId, connection)
    local specMixerWagon = self.spec_mixerWagon;

    for _, entry in ipairs(specMixerWagon.mixerWagonFillTypes) do
        streamWriteFloat32(streamId, entry.fillLevel);
    end;

    streamWriteInt32(streamId, specMixerWagon.currentMixingTime);
end;

function MixerWagon:onReadUpdateStream(streamId, timestamp, connection)
    if connection:getIsServer() then
        if streamReadBool(streamId) then
            local specMixerWagon = self.spec_mixerWagon;
            
            for _, entry in ipairs(specMixerWagon.mixerWagonFillTypes) do
                local fillLevel = streamReadFloat32(streamId);
                local delta = fillLevel - entry.fillLevel;

                if delta ~= 0 then
                    self:addFillUnitFillLevel(self:getOwnerFarmId(), specMixerWagon.fillUnitIndex, delta, next(entry.fillTypes), ToolType.UNDEFINED, nil);
                end;
            end;

            specMixerWagon.currentMixingTime = streamReadInt32(streamId);
        end;
    end;
end;

function MixerWagon:onWriteUpdateStream(streamId, connection, dirtyMask)
    if not connection:getIsServer() then
        local specMixerWagon = self.spec_mixerWagon;

        if streamWriteBool(streamId, bitAND(dirtyMask, specMixerWagon.dirtyFlag) ~= 0) then
            for _, entry in ipairs(specMixerWagon.mixerWagonFillTypes) do
                streamWriteFloat32(streamId, entry.fillLevel);
            end;

            streamWriteInt32(streamId, specMixerWagon.currentMixingTime);
        end;
    end;
end;

function MixerWagon:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local specMixerWagon = self.spec_mixerWagon;
    local specMotorized = self.spec_motorized;

    local tipState = self:getTipState();
    local isTurnedOn = self:getIsTurnedOn();
    local isDischarging = tipState == Trailer.TIPSTATE_OPENING or tipState == Trailer.TIPSTATE_OPEN;

    specMixerWagon.isFirstRun = false;

    self:raiseActive();

    if specMixerWagon.forceUpdate or isTurnedOn or isDischarging or not specMixerWagon.needTurnOn then
        g_animationManager:startAnimations(specMixerWagon.mixAnimationNodes);
        local fillLevel = self:getFillUnitFillLevel(specMixerWagon.fillUnitIndex);
        
        if not isDischarging then
            if fillLevel > 0 then
                if specMixerWagon.requiredMixingTime > 0 and specMixerWagon.forageOk or #specMixerWagon.mixerWagonFillTypes == 1 then    
                    local maxClamp = 0;

                    if not specMixerWagon.isSingleFilled or #specMixerWagon.mixerWagonFillTypes == 1 then
                        maxClamp = (specMixerWagon.requiredMixingTime * 1000) / 3;

                        if specMixerWagon.currentMixingTime > maxClamp then
                            maxClamp = specMixerWagon.currentMixingTime;
                        end;
                    end;

                    if specMixerWagon.forageOk or #specMixerWagon.mixerWagonFillTypes == 1 then
                        maxClamp = specMixerWagon.requiredMixingTime * 1000;
                    end;

                    local currentMixingTime = MathUtil.clamp((specMixerWagon.currentMixingTime + dt), 0, maxClamp);

                    self:setMixingTime(currentMixingTime);

                    self:calculatePercentage();
                end;

                local fillType = specMixerWagon.failedOutputFillType;

                if specMixerWagon.mixingTimePercentage == 1 or specMixerWagon.requiredMixingTime == 0 then
                    fillType = specMixerWagon.outputFillType;
                    
                    if specMotorized ~= nil then
                        specMotorized.motorStopTimerDuration = specMotorized.motorStopTimerDurationBackUp;
                    end;
                else
                    if specMotorized ~= nil then
                        specMotorized.motorStopTimerDuration = math.huge;
                    end;
                end;

                self:addFillUnitFillLevel(self:getOwnerFarmId(), specMixerWagon.fillUnitIndex, 0, fillType, ToolType.UNDEFINED, nil);
            else
                if specMotorized ~= nil then
                    specMotorized.motorStopTimerDuration = specMotorized.motorStopTimerDurationBackUp;
                end;
            end;
        end;

        specMixerWagon.forceUpdate = false;
    else
        g_animationManager:stopAnimations(specMixerWagon.mixAnimationNodes);
    end;
end;

function MixerWagon:mixerWagonBaleTriggerCallback(triggerId, otherActorId, onEnter, onLeave, onStay, otherShapeId)
    if onEnter then
        --## this happens if a compound child of a deleted compound is entering
        if otherActorId ~= 0 then
            local bale = g_currentMission:getNodeObject(otherActorId);

            if bale ~= nil then
                if bale:isa(Bale) then
                    local specMixerWagon = self.spec_mixerWagon;
                    local fillLevel = bale:getFillLevel();
                    local fillTypeIndex = bale:getFillType();

                    if self:getFillUnitSupportsFillType(specMixerWagon.fillUnitIndex, fillTypeIndex) and self:getFillUnitFreeCapacity(specMixerWagon.fillUnitIndex) > 0 then
                        self:addFillUnitFillLevel(self:getOwnerFarmId(), specMixerWagon.fillUnitIndex, fillLevel, fillTypeIndex, ToolType.BALE, nil);
                        
                        specMixerWagon.forceUpdate = true;

                        if specMixerWagon.requiredMixingTime > 0 then
                            self:resetMixingTime();
                        end;

                        bale:delete();
                        
                        self:raiseDirtyFlags(specMixerWagon.dirtyFlag);
                    else
                        if self.isClient then
                            g_currentMission:addIngameNotification(FSBaseMission.INGAME_NOTIFICATION_CRITICAL, g_i18n:getText("warning_baleNotSupported"));
                        end;

                        g_server:broadcastEvent(MixerWagonBaleNotAcceptedEvent:new(self), nil, nil, self);
                    end;
                end;
            end;
        end;
    end;
end;

function MixerWagon:addFillUnitFillLevel(superFunc, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData)
	local specMixerWagon = self.spec_mixerWagon;

    if fillUnitIndex ~= specMixerWagon.fillUnitIndex then
		return superFunc(self, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData);
    end;
    
	local oldFillLevel = self:getFillUnitFillLevel(fillUnitIndex);
    local mixerWagonFillType = specMixerWagon.fillTypeToMixerWagonFillType[fillTypeIndex];

    --[[## allow to put output fill type back to mixer and split it up into valid forage mixing ratios
    
    --## !!! ATTENTION !!! Currently deactivated, because this will not work for Clients !!!
    
    if fillTypeIndex == specMixerWagon.outputFillType and fillLevelDelta > 0 then
        for _, mixerWagonFillType in pairs(specMixerWagon.mixerWagonFillTypes) do
            if oldFillLevel == 0 then
                mixerWagonFillType.fillLevel = mixerWagonFillType.maxPercentage - (mixerWagonFillType.maxPercentage / 2);

                self:setMixingTime(specMixerWagon.requiredMixingTime * 1000);
                self:calculatePercentage();

                self:raiseDirtyFlags(specMixerWagon.dirtyFlag);
            end;
        end;
        
        return superFunc(self, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData);
	end;]]

	if mixerWagonFillType == nil then
		--## used for discharge
		if fillLevelDelta < 0 and oldFillLevel > 0 then
			--## remove values from all fill types such that the ratio doesn't change
			fillLevelDelta = math.max(fillLevelDelta, -oldFillLevel);
			local newFillLevel = 0;

			for _, mixerWagonFillType in pairs(specMixerWagon.mixerWagonFillTypes) do
				local entryDelta = fillLevelDelta * (mixerWagonFillType.fillLevel / oldFillLevel);
                
                mixerWagonFillType.fillLevel = math.max(mixerWagonFillType.fillLevel + entryDelta, 0);
                
                newFillLevel = newFillLevel + mixerWagonFillType.fillLevel;
            end;
            
            if newFillLevel <= 0 and specMixerWagon.requiredMixingTime > 0 then
                self:resetMixingTime();
            end;

            self:raiseDirtyFlags(specMixerWagon.dirtyFlag);

			return superFunc(self, farmId, fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData);
		end;


		return 0;
	end;
    
    local capacity = self:getFillUnitCapacity(fillUnitIndex);
    local free = capacity - oldFillLevel;

	if fillLevelDelta > 0 then
		mixerWagonFillType.fillLevel = mixerWagonFillType.fillLevel + math.min(free, fillLevelDelta);

        specMixerWagon.forceUpdate = true;

        if specMixerWagon.requiredMixingTime > 0 and fillTypeIndex ~= specMixerWagon.outputFillType then
            if not specMixerWagon.isFirstRun then
                self:resetMixingTime();
            end;
        end;
	else
		mixerWagonFillType.fillLevel = math.max(0, mixerWagonFillType.fillLevel + fillLevelDelta);
	end;

	local newFillLevel = 0;

	for _, mixerWagonFillType in pairs(specMixerWagon.mixerWagonFillTypes) do
		newFillLevel = newFillLevel + mixerWagonFillType.fillLevel;
	end;

	newFillLevel = MathUtil.clamp(newFillLevel, 0, self:getFillUnitCapacity(fillUnitIndex));
	
	local newFillType = FillType.UNKNOWN;
	local isSingleFilled = false;
    local isForageOk = false;

    if #specMixerWagon.mixerWagonFillTypes > 1 then
	    for _, mixerWagonFillType in pairs(specMixerWagon.mixerWagonFillTypes) do
	    	if newFillLevel == mixerWagonFillType.fillLevel then
	    		isSingleFilled = true;
                newFillType = next(mixerWagonFillType.fillTypes);
            
	    		break;
	    	end;
        end;
    else
        if specMixerWagon.mixingTimePercentage > 0.999 or specMixerWagon.requiredMixingTime == 0 then    
            newFillType = specMixerWagon.outputFillType;
        else
            newFillType = specMixerWagon.failedOutputFillType;
        end;

        isSingleFilled = true;
    end;

	if not isSingleFilled then
		isForageOk = true;

		for _, mixerWagonFillType in pairs(specMixerWagon.mixerWagonFillTypes) do
			if mixerWagonFillType.fillLevel < mixerWagonFillType.minPercentage * newFillLevel - 0.01 or mixerWagonFillType.fillLevel > mixerWagonFillType.maxPercentage * newFillLevel + 0.01 then
				isForageOk = false;
				
				break;
			end;
		end;
    end;
    
    specMixerWagon.forageOk = isForageOk;
    specMixerWagon.isSingleFilled = isSingleFilled;
    specMixerWagon.newFillLevel = newFillLevel;
    
	if isForageOk then
        if specMixerWagon.mixingTimePercentage > 0.999 or specMixerWagon.requiredMixingTime == 0 then    
            newFillType = specMixerWagon.outputFillType;
        else
            newFillType = specMixerWagon.failedOutputFillType;
        end;
	elseif not isSingleFilled then
		newFillType = specMixerWagon.failedOutputFillType;
    end;
    
    self:raiseDirtyFlags(specMixerWagon.dirtyFlag);
    self:setFillUnitFillType(fillUnitIndex, newFillType);

	return superFunc(self, farmId, fillUnitIndex, newFillLevel-oldFillLevel, newFillType, toolType, fillPositionData);
end;

function MixerWagon:getFillUnitAllowsFillType(superFunc, fillUnitIndex, fillTypeIndex)
    local specMixerWagon = self.spec_mixerWagon;

    if specMixerWagon.fillUnitIndex == fillUnitIndex then
        local mixerWagonFillType = specMixerWagon.fillTypeToMixerWagonFillType[fillTypeIndex];

        if mixerWagonFillType ~= nil then
            return true;
        end;
    end;

    return superFunc(self, fillUnitIndex, fillTypeIndex);
end;

function MixerWagon:getDischargeFillType(superFunc, dischargeNode)
    local specMixerWagon = self.spec_mixerWagon;
    local fillUnitIndex = dischargeNode.fillUnitIndex;

    if specMixerWagon ~= nil and fillUnitIndex == specMixerWagon.fillUnitIndex then
        local currentFillType = self:getFillUnitFillType(fillUnitIndex);
        local fillLevel = self:getFillUnitFillLevel(fillUnitIndex);

        if currentFillType == specMixerWagon.failedOutputFillType and fillLevel > 0 then
            for _, entry in pairs(specMixerWagon.mixerWagonFillTypes) do
                if entry.fillLevel > 0 then
                    currentFillType = next(entry.fillTypes);
                    
                    break;
                end;
            end;
        end;

        return currentFillType;
    end;

    return superFunc(self, dischargeNode);
end;

function MixerWagon:onFillUnitFillLevelChanged(fillUnitIndex, fillLevelDelta, fillTypeIndex, toolType, fillPositionData, appliedDelta)
    local specMixerWagon = self.spec_mixerWagon;

    if specMixerWagon.fillUnitIndex == fillUnitIndex then
        local fillLevel = self:getFillUnitFillLevel(fillUnitIndex);

        if fillLevel == 0 then
            for _, entry in pairs(specMixerWagon.mixerWagonFillTypes) do
                entry.fillLevel = 0;
            end;
        end;
    end;
end;

function MixerWagon:onTurnedOn()
    if self.isClient then
        local specMixerWagon = self.spec_mixerWagon;

        g_animationManager:startAnimations(specMixerWagon.pickupAnimationNodes);
    end;
end;

function MixerWagon:onTurnedOff()
    if self.isClient then
        local specMixerWagon = self.spec_mixerWagon;

        g_animationManager:stopAnimations(specMixerWagon.pickupAnimationNodes);
    end;
end;

MixingTimeEvent = {};

local MixingTimeEvent_mt = Class(MixingTimeEvent, Event);

InitEventClass(MixingTimeEvent, "MixingTimeEvent");

function MixingTimeEvent:emptyNew()
    local self = Event:new(MixingTimeEvent_mt);

    return self;
end;

function MixingTimeEvent:new(mixerWagon, mixingTime)
    local self = MixingTimeEvent:emptyNew();
    
    self.mixerWagon = mixerWagon;
    self.mixingTime = mixingTime;

    return self;
end;

function MixingTimeEvent:readStream(streamId, connection)
    self.mixerWagon = NetworkUtil.readNodeObject(streamId);
    self.mixingTime = streamReadInt32(streamId);

    self:run(connection);
end;

function MixingTimeEvent:writeStream(streamId, connection)
    NetworkUtil.writeNodeObject(streamId, self.mixerWagon);
    
    streamWriteInt32(streamId, self.mixingTime);
end;

function MixingTimeEvent:run(connection)
    if not connection:getIsServer() then
        g_server:broadcastEvent(self, false, connection, self.mixerWagon);
    end;

    if self.mixerWagon ~= nil then
        self.mixerWagon:setMixingTime(self.mixingTime);
    end;
end;

function MixingTimeEvent.sendEvent(mixerWagon, mixingTime, noEventSend)
    if noEventSend == nil or noEventSend == false then
        if g_server ~= nil then
            g_server:broadcastEvent(MixingTimeEvent:new(mixerWagon, mixingTime), nil, nil, mixerWagon);
        else
            g_client:getServerConnection():sendEvent(MixingTimeEvent:new(mixerWagon, mixingTime));
        end;
    end;
end;