--[[
Copyright (C) GtX (Andy), 2022

Author: GtX | Andy
Date: 05.04.2022
Revision: FS25-02

Contact:
https://forum.giants-software.com
https://github.com/GtX-Andy

Important:
Free for use in mods (FS25 Only) - no permission needed.
No modifications may be made to this script, including conversion to other game versions without written permission from GtX | Andy
Copying or removing any part of this code for external use without written permission from GtX | Andy is prohibited.

Frei verwendbar (Nur LS25) - keine erlaubnis nötig
Ohne schriftliche Genehmigung von GtX | Andy dürfen keine Änderungen an diesem Skript vorgenommen werden, einschließlich der Konvertierung in andere Spielversionen
Das Kopieren oder Entfernen irgendeines Teils dieses Codes zur externen Verwendung ohne schriftliche Genehmigung von GtX | Andy ist verboten.
]]


PlaceableSeedCleaner = {}

PlaceableSeedCleaner.MOD_NAME = g_currentModName
PlaceableSeedCleaner.SPEC_NAME = string.format("%s.seedCleaner", g_currentModName)
PlaceableSeedCleaner.SPEC_TABLE_NAME = string.format("spec_%s", PlaceableSeedCleaner.SPEC_NAME)

function PlaceableSeedCleaner.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(PlaceableInfoTrigger, specializations)
end

function PlaceableSeedCleaner.initSpecialization()
    g_placeableConfigurationManager:addConfigurationType("seedCleanerDesign", g_i18n:getText("configuration_design"), "seedCleaner", PlaceableConfigurationItem)
    g_placeableConfigurationManager:addConfigurationType("seedCleanerProduction", g_i18n:getText("ui_productions_production"), "seedCleaner", PlaceableConfigurationItem)
    g_placeableConfigurationManager:addConfigurationType("seedCleanerDisplay", g_i18n:getText("ui_ingameMenuGameSettingsDisplay"), "seedCleaner", PlaceableConfigurationItem)
end

function PlaceableSeedCleaner.registerOverwrittenFunctions(placeableType)
    SpecializationUtil.registerOverwrittenFunction(placeableType, "setOwnerFarmId", PlaceableSeedCleaner.setOwnerFarmId)
    SpecializationUtil.registerOverwrittenFunction(placeableType, "updateInfo", PlaceableSeedCleaner.updateInfo)
    SpecializationUtil.registerOverwrittenFunction(placeableType, "collectPickObjects", PlaceableSeedCleaner.collectPickObjects)
end

function PlaceableSeedCleaner.registerFunctions(placeableType)
    SpecializationUtil.registerFunction(placeableType, "setSeedCleanerState", PlaceableSeedCleaner.setSeedCleanerState)
end

function PlaceableSeedCleaner.registerEventListeners(placeableType)
    SpecializationUtil.registerEventListener(placeableType, "onLoad", PlaceableSeedCleaner)
    SpecializationUtil.registerEventListener(placeableType, "onFinalizePlacement", PlaceableSeedCleaner)
    SpecializationUtil.registerEventListener(placeableType, "onDelete", PlaceableSeedCleaner)
    SpecializationUtil.registerEventListener(placeableType, "onUpdate", PlaceableSeedCleaner)
    SpecializationUtil.registerEventListener(placeableType, "onWriteStream", PlaceableSeedCleaner)
    SpecializationUtil.registerEventListener(placeableType, "onReadStream", PlaceableSeedCleaner)
end

function PlaceableSeedCleaner.registerXMLPaths(schema, basePath)
    schema:setXMLSpecializationType("SeedCleaner")

    local configKey = basePath .. ".seedCleaner.seedCleanerProductionConfigurations.seedCleanerProductionConfiguration(?)"

    schema:register(XMLValueType.BOOL, configKey .. "#useRealTime", "If the conversion process should be in real time or game time.", false)
    schema:register(XMLValueType.BOOL, basePath .. ".seedCleaner.seedCleanerProductionConfigurations#infoTriggerDisplay", "Determines if the selected configuration name will be displayed in the info trigger box.", true)

    schema:register(XMLValueType.FLOAT, basePath .. ".seedCleaner.treatment#usagePerLitre", "Usage of treatment liquid. Min 0.0001", 0.1)
    schema:register(XMLValueType.STRING, basePath .. ".seedCleaner.treatment#fillType", "Treatment liquid fill type", "INVALID")

    schema:register(XMLValueType.STRING, basePath .. ".seedCleaner.rawMaterials#convertedFillType", "Converted fill type", "SEEDS")
    schema:register(XMLValueType.FLOAT, basePath .. ".seedCleaner.rawMaterials#conversionPerMinute", "Speed factor, litres per minute to be converted. Min 60 / Max 6000", 60)
    schema:register(XMLValueType.FLOAT, basePath .. ".seedCleaner.rawMaterials#conversionPerSecond", "Speed factor, litres per second to be converted. Min 1 / Max 100", 1)
    schema:register(XMLValueType.FLOAT, basePath .. ".seedCleaner.rawMaterials#sharedFactor", "Shared conversion factor: Min 1 / Max 5", 1)
    schema:register(XMLValueType.STRING, basePath .. ".seedCleaner.rawMaterials.rawMaterial(?)#fillType", "Accepted raw material fill type", "INVALID")
    schema:register(XMLValueType.FLOAT, basePath .. ".seedCleaner.rawMaterials.rawMaterial(?)#factor", "Individual conversion factor: Min 1 / Max 5", "sharedFactor")

    UnloadingStation.registerXMLPaths(schema, basePath .. ".seedCleaner.unloadingStation")
    LoadingStation.registerXMLPaths(schema, basePath .. ".seedCleaner.loadingStation")
    Storage.registerXMLPaths(schema, basePath .. ".seedCleaner.storage")

    schema:register(XMLValueType.STRING, basePath .. ".seedCleaner.fillPlanes.fillPlane(?)#categoryType", "Category name. Options: rawMaterialsType | treatmentType | convertedType", "convertedType")
    schema:register(XMLValueType.FLOAT, basePath .. ".seedCleaner.fillPlanes.fillPlane(?)#minScaleY", "Fill plane min scale y")
    schema:register(XMLValueType.FLOAT, basePath .. ".seedCleaner.fillPlanes.fillPlane(?)#maxScaleY", "Fill plane max scale y")
    schema:register(XMLValueType.BOOL, basePath .. ".seedCleaner.fillPlanes.fillPlane(?)#isCustomShape", "Is Custom Shape")
    FillPlane.registerXMLPaths(schema, basePath .. ".seedCleaner.fillPlanes.fillPlane(?)")

    configKey = basePath .. ".seedCleaner.seedCleanerDisplayConfigurations.seedCleanerDisplayConfiguration(?)"

    schema:register(XMLValueType.NODE_INDEX, configKey .. ".display(?)#node", "Display start node")
    schema:register(XMLValueType.STRING, configKey .. ".display(?)#categoryType", "Category name. Options: rawMaterialsType | treatmentType | convertedType", "convertedType")
    schema:register(XMLValueType.STRING, configKey .. ".display(?)#font", "Display font name")
    schema:register(XMLValueType.STRING, configKey .. ".display(?)#alignment", "Display text alignment")
    schema:register(XMLValueType.FLOAT, configKey .. ".display(?)#size", "Display text size", 0.03)
    schema:register(XMLValueType.FLOAT, configKey .. ".display(?)#scaleX", "Display text x scale", 1)
    schema:register(XMLValueType.FLOAT, configKey .. ".display(?)#scaleY", "Display text y scale", 1)
    schema:register(XMLValueType.STRING, configKey .. ".display(?)#mask", "Display text mask", "0000000")
    schema:register(XMLValueType.FLOAT, configKey .. ".display(?)#emissiveScale", "Display emissive scale", 0.2)
    schema:register(XMLValueType.COLOR, configKey .. ".display(?)#color", "Display text colour", "0.9 0.9 0.9 1")
    schema:register(XMLValueType.COLOR, configKey .. ".display(?)#hiddenColor", "Display text hidden colour")
    schema:register(XMLValueType.STRING, configKey .. ".display(?)#type", "Display type. Options: fillLevel | percent | capacity")

    SoundManager.registerSampleXMLPaths(schema, basePath .. ".seedCleaner.sounds", "start")
    SoundManager.registerSampleXMLPaths(schema, basePath .. ".seedCleaner.sounds", "stop")
    SoundManager.registerSampleXMLPaths(schema, basePath .. ".seedCleaner.sounds", "work")

    ObjectChangeUtil.registerObjectChangeXMLPaths(schema, basePath .. ".seedCleaner.objectChanges")
    AnimationManager.registerAnimationNodesXMLPaths(schema, basePath .. ".seedCleaner.animationNodes")
    EffectManager.registerEffectXMLPaths(schema, basePath .. ".seedCleaner.effectNodes")

    schema:setXMLSpecializationType()
end

function PlaceableSeedCleaner.registerSavegameXMLPaths(schema, basePath)
    schema:setXMLSpecializationType("SeedCleaner")

    Storage.registerSavegameXMLPaths(schema, basePath)

    schema:setXMLSpecializationType()
end

function PlaceableSeedCleaner:onLoad(savegame)
    self.spec_seedCleaner = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

    if self.spec_seedCleaner == nil then
        Logging.error("[%s] Specialisation with name 'seedCleaner' was not found in modDesc!", PlaceableSeedCleaner.MOD_NAME)
    end

    local spec = self.spec_seedCleaner
    local xmlFile = self.xmlFile

    local configurationId = Utils.getNoNil(self.configurations.seedCleanerProduction, 1)
    local configKey = string.format("placeable.seedCleaner.seedCleanerProductionConfigurations.seedCleanerProductionConfiguration(%d)", configurationId - 1)

    spec.useRealTime = xmlFile:getValue(configKey .. "#useRealTime", false)
    spec.infoTableShowConfiguration = xmlFile:getValue("placeable.seedCleaner.seedCleanerProductionConfigurations#infoTriggerDisplay", true)

    spec.texts = {
        defaultInputTitle = g_i18n:getText("contract_details_harvesting_crop"),
        litresFormat = "%s " .. g_i18n:getText("unit_literShort"),
        active = g_i18n:getText("ui_production_status_running"),
        inactive = g_i18n:getText("ui_production_status_inactive"),
        outOfSpace = g_i18n:getText("ui_production_status_outOfSpace"),
        materialsMissing = g_i18n:getText("ui_production_status_materialsMissing"),
    }

    spec.infoTableStatus = {
        accentuate = true,
        title = g_i18n:getText("ui_productions_status")
    }

    spec.infoTableState = {
        title = "",
        text = ""
    }

    spec.infoTableStorage = {
        accentuate = true,
        title = g_i18n:getText("ui_productions_buildingStorage")
    }

    spec.infoTableProductionState = {
        accentuate = true,
        title = g_i18n:getText("ui_productions_production")
    }

    spec.infoTableProduction = {
        title = "",
        text = spec.useRealTime and g_i18n:getText("ui_realTime") or g_i18n:getText("configuration_valueDefault")
    }

    spec.active = false
    spec.updateTime = 0

    spec.treatmentUsage = math.max(xmlFile:getValue("placeable.seedCleaner.treatment#usagePerLitre", 0.1), 0.0001)
    spec.treatmentFillType = g_fillTypeManager:getFillTypeIndexByName(xmlFile:getValue("placeable.seedCleaner.treatment#fillType", "invalid"))

    if spec.treatmentFillType == nil then
        if g_iconGenerator ~= nil then
            spec.treatmentFillType = FillType.HERBICIDE
        end

        Logging.xmlError(self.xmlFile, "Failed to load treatment fill type at 'placeable.seedCleaner.treatment#fillType'!")

        self:setLoadingState(PlaceableLoadingState.ERROR)

        return
    end

    spec.convertedFillType = g_fillTypeManager:getFillTypeIndexByName(xmlFile:getValue("placeable.seedCleaner.rawMaterials#convertedFillType", "seeds")) or FillType.SEEDS

    local conversionPerSecond = xmlFile:getValue("placeable.seedCleaner.rawMaterials#conversionPerMinute", 60) / 60
    spec.conversionPerMs = math.clamp(xmlFile:getValue("placeable.seedCleaner.rawMaterials#conversionPerSecond", conversionPerSecond), 1, 100) / 1000

    spec.sharedFactor = math.clamp(xmlFile:getValue("placeable.seedCleaner.rawMaterials#sharedFactor", 1), 1, 5)

    spec.rawMaterialFactors = {}
    spec.sortedRawMaterialFillTypes = {}

    for _, rawMaterialKey in xmlFile:iterator("placeable.seedCleaner.rawMaterials.rawMaterial") do
        local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(xmlFile:getValue(rawMaterialKey .. "#fillType", "invalid"))

        if fillTypeIndex ~= nil and spec.rawMaterialFactors[fillTypeIndex] == nil then
            local factor = math.clamp(xmlFile:getValue(rawMaterialKey .. "#factor", spec.sharedFactor), 1, 5)

            spec.rawMaterialFactors[fillTypeIndex] = factor

            table.insert(spec.sortedRawMaterialFillTypes, {
                index = fillTypeIndex,
                factor = factor
            })
        end
    end

    table.sort(spec.sortedRawMaterialFillTypes, function(a, b)
        return a.index < b.index
    end)

    if #spec.sortedRawMaterialFillTypes == 0 then
        Logging.xmlError(self.xmlFile, "No valid raw materials found for conversion to '%s'!", g_fillTypeManager:getFillTypeNameByIndex(spec.convertedFillType))

        self:setLoadingState(PlaceableLoadingState.ERROR)

        return
    end

    spec.unloadingStation = UnloadingStation.new(self.isServer, self.isClient)

    if spec.unloadingStation:load(self.components, xmlFile, "placeable.seedCleaner.unloadingStation", self.customEnvironment, self.i3dMappings, self.components[1].node) then
        spec.unloadingStation.owningPlaceable = self
        spec.unloadingStation.hasStoragePerFarm = false
        spec.unloadingStation.supportsExtension = false
    else
        Logging.xmlError(self.xmlFile, "Failed to load 'unloadingStation'!")

        self:setLoadingState(PlaceableLoadingState.ERROR)

        return
    end

    spec.loadingStation = LoadingStation.new(self.isServer, self.isClient)

    if spec.loadingStation:load(self.components, xmlFile, "placeable.seedCleaner.loadingStation", self.customEnvironment, self.i3dMappings, self.components[1].node) then
        spec.loadingStation.owningPlaceable = self
        spec.loadingStation.hasStoragePerFarm = false

        local oldAddSourceStorage = spec.loadingStation.addSourceStorage

        spec.loadingStation.addSourceStorage = function (loadingStation, storage)
            if storage == nil or storage ~= spec.storage then
                return false
            end

            oldAddSourceStorage(loadingStation, storage)
        end

        spec.loadingStation.getAllFillLevels = function (loadingStation, farmId)
            local fillLevels = {}

            for _, sourceStorage in pairs(loadingStation.sourceStorages) do
                if loadingStation:hasFarmAccessToStorage(farmId, sourceStorage) then
                    for fillType, fillLevel in pairs(sourceStorage:getFillLevels()) do
                        if fillType == spec.convertedFillType or fillLevel > 0.1 then
                            fillLevels[fillType] = (fillLevels[fillType] or 0) + fillLevel
                        end
                    end
                end
            end

            return fillLevels
        end
    else
        Logging.xmlError(self.xmlFile, "Failed to load 'loadingStation'!")

        self:setLoadingState(PlaceableLoadingState.ERROR)

        return
    end

    spec.storage = Storage.new(self.isServer, self.isClient)

    if spec.storage:load(self.components, xmlFile, "placeable.seedCleaner.storage", self.i3dMappings) then
        spec.storage.dynamicFillPlaneBaseNode = nil
        spec.storage.dynamicFillPlane = nil

        spec.storage.getFreeCapacity = function (storage, fillType)
            if storage.fillLevels[fillType] == nil then
                return 0
            end

            local capacity = storage.capacities[fillType] or storage.capacity

            if fillType == spec.convertedFillType or fillType == spec.treatmentFillType or fillType == spec.rawMaterialFillType then
                return math.max(capacity - storage.fillLevels[fillType], 0)
            end

            local usedCapacity = 0

            for _, rawMaterialFillType in ipairs(spec.sortedRawMaterialFillTypes) do
                if fillType == rawMaterialFillType.index then
                    usedCapacity = storage:getFillLevel(rawMaterialFillType.index)
                elseif storage:getFillLevel(rawMaterialFillType.index) > 0.1 then
                    return 0
                end
            end

            return math.max(capacity - usedCapacity, 0)
        end

        spec.storage.setFillLevel = function(storage, fillLevel, fillType, fillInfo)
            if storage.fillLevels[fillType] ~= nil then
                local capacity = storage.capacities[fillType] or storage.capacity

                fillLevel = math.clamp(fillLevel, 0, capacity)

                if self.isServer and spec.rawMaterialFactors[fillType] ~= nil then
                    if spec.rawMaterialFillType ~= nil and spec.rawMaterialFillType ~= fillType then
                        if storage:getFillLevel(spec.rawMaterialFillType) > 0 then
                            return
                        end
                    end

                    if fillLevel > 0 then
                        spec.rawMaterialFillType = fillType
                    else
                        spec.rawMaterialFillType = nil
                    end
                end

                if fillLevel ~= storage.fillLevels[fillType] then
                    local oldLevel = storage.fillLevels[fillType]

                    storage.fillLevels[fillType] = fillLevel

                    local delta = storage.fillLevels[fillType] - oldLevel
                    local newFillLevelInt = MathUtil.round(fillLevel)

                    if math.abs(delta) > 0.1 or (storage.fillLevelsLastPublished[fillType] ~= newFillLevelInt) then
                        for _, func in ipairs(storage.fillLevelChangedListeners) do
                            func(fillType, delta)
                        end

                        storage.fillLevelsLastPublished[fillType] = newFillLevelInt
                    end

                    if self.isServer then
                        if fillLevel < 0.1 or storage.fillLevelSyncThreshold <= math.abs(storage.fillLevelsLastSynced[fillType] - fillLevel) or capacity - fillLevel < 0.1 then
                            storage:raiseDirtyFlags(storage.storageDirtyFlag)
                        end

                        if not spec.active and not spec.timerSet and spec.rawMaterialFillType ~= nil then
                            if storage:getFreeCapacity(spec.convertedFillType) > 0 and storage:getFillLevel(spec.treatmentFillType) > 0 then
                                if storage:getFillLevel(spec.rawMaterialFillType) > 0 then
                                    Timer.createOneshot(math.max(spec.conversionPerMs * 1000, 5000), function ()
                                        self:setSeedCleanerState(true)
                                        spec.timerSet = false
                                    end)

                                    spec.timerSet = true
                                end
                            end
                        end
                    end

                    if storage.isClient then
                        if spec.fillPlanes ~= nil and spec.fillPlanes[fillType] ~= nil then
                            local percent = 0

                            if capacity > 0 then
                                percent = fillLevel / capacity
                            end

                            for _, fillPlane in ipairs (spec.fillPlanes[fillType]) do
                                if fillPlane.fillType ~= fillType then
                                    fillPlane.fillType = fillType

                                    if fillPlane.isCustomShape then
                                        FillPlaneUtil.assignDefaultMaterialsFromTerrain(fillPlane.node, g_terrainNode)
                                        FillPlaneUtil.setFillType(fillPlane.node, fillType)

                                        setShaderParameter(fillPlane.node, "isCustomShape", 1, 0, 0, 0, false)
                                    end
                                end

                                fillPlane:setState(percent)
                            end
                        end

                        if spec.displays ~= nil and spec.displays[fillType] ~= nil then
                            for _, display in ipairs (spec.displays[fillType]) do
                                display:onFillLevelChange(fillLevel, capacity, fillType)
                            end
                        end
                    end
                end
            end
        end
    else
        Logging.xmlError(self.xmlFile, "Failed to load 'storage'!")

        self:setLoadingState(PlaceableLoadingState.ERROR)

        return
    end

    if self.isClient then
        local function getCategoryTypeFillTypes(name)
            name = name:upper()

            if name == "RAWMATERIALSTYPE" then
                return spec.rawMaterialFactors, name
            end

            if name == "TREATMENTTYPE" then
                return {[spec.treatmentFillType] = 1}, name
            end

            return {[spec.convertedFillType] = 1}, "CONVERTEDTYPE"
        end

        if xmlFile:hasProperty("placeable.seedCleaner.fillPlanes") then
            spec.fillPlanes = {}

            for _, fillPlaneKey in xmlFile:iterator("placeable.seedCleaner.fillPlanes.fillPlane") do
                local fillTypes, categoryType = getCategoryTypeFillTypes(xmlFile:getValue(fillPlaneKey .. "#categoryType", "convertedType"))

                if fillTypes ~= nil then
                    local fillPlane = FillPlane.new()

                    if fillPlane:load(self.components, xmlFile, fillPlaneKey, self.i3dMappings) then
                        fillPlane.isCustomShape = xmlFile:getValue(fillPlaneKey .. "#isCustomShape", false)

                        if fillPlane.isCustomShape then
                            local fillTypeIndex = next(fillTypes)

                            if fillTypeIndex ~= nil then
                                FillPlaneUtil.assignDefaultMaterialsFromTerrain(fillPlane.node, g_terrainNode)
                                FillPlaneUtil.setFillType(fillPlane.node, fillTypeIndex)

                                setShaderParameter(fillPlane.node, "isCustomShape", 1, 0, 0, 0, false)
                            end
                        end

                        local scaleMinY = xmlFile:getValue(fillPlaneKey .. "#minScaleY")
                        local scaleMaxY = xmlFile:getValue(fillPlaneKey .. "#maxScaleY")

                        if scaleMinY ~= nil or scaleMaxY ~= nil then
                            local sx, sy, sz = getScale(fillPlane.node)

                            fillPlane.scaleMinY = scaleMinY or sy
                            fillPlane.scaleMaxY = scaleMaxY or sy

                            setScale(fillPlane.node, sx, fillPlane.scaleMinY, sz)

                            fillPlane.setState = Utils.overwrittenFunction(fillPlane.setState, function(fillPlane, superFunc, state)
                                if fillPlane.scaleMinY ~= nil then
                                    local sx, sy, sz = getScale(fillPlane.node)

                                    sy = MathUtil.lerp(fillPlane.scaleMinY, fillPlane.scaleMaxY, state)

                                    setScale(fillPlane.node, sx, sy, sz)
                                end

                                return superFunc(fillPlane, state)
                            end)
                        end

                        fillPlane.categoryType = categoryType

                        for fillTypeIndex, _ in pairs(fillTypes) do
                            if spec.fillPlanes[fillTypeIndex] == nil then
                                spec.fillPlanes[fillTypeIndex] = {}
                            end

                            table.insert(spec.fillPlanes[fillTypeIndex], fillPlane)
                        end
                    else
                        fillPlane:delete()
                    end
                end
            end

            if next(spec.fillPlanes) == nil then
                spec.fillPlanes = nil
            end
        end

        if xmlFile:hasProperty("placeable.seedCleaner.seedCleanerDisplayConfigurations") then
            configurationId = Utils.getNoNil(self.configurations.seedCleanerDisplay, 1)
            configKey = string.format("placeable.seedCleaner.seedCleanerDisplayConfigurations.seedCleanerDisplayConfiguration(%d)", configurationId - 1)

            spec.displays = {}

            for _, displayKey in xmlFile:iterator(configKey .. ".display") do
                local displayNode = xmlFile:getValue(displayKey .. "#node", nil, self.components, self.i3dMappings)

                if displayNode ~= nil then
                    local fontName = xmlFile:getValue(displayKey .. "#font", "digit"):upper()
                    local fontMaterial = g_materialManager:getFontMaterial(fontName, self.customEnvironment)

                    if fontMaterial ~= nil then
                        local mask = xmlFile:getValue(displayKey .. "#mask", "0000000")
                        local hiddenColour = xmlFile:getValue(displayKey .. "#hiddenColor", nil, true)

                        local display = {
                            displayNode = displayNode,
                            fontMaterial = fontMaterial,
                            hiddenColor = hiddenColour
                        }

                        display.formatStr, display.formatPrecision = Utils.maskToFormat(mask)
                        display.onFillLevelChange, display.typeName = PlaceableSeedCleaner.getDisplayFunction(xmlFile:getValue(displayKey .. "#type", "fillLevel"):upper())

                        if display.onFillLevelChange ~= nil then
                            local alignmentStr = xmlFile:getValue(displayKey .. "#alignment", "RIGHT")
                            local alignment = RenderText["ALIGN_" .. alignmentStr:upper()] or RenderText.ALIGN_RIGHT

                            local size = xmlFile:getValue(displayKey .. "#size", 0.03)
                            local scaleX = xmlFile:getValue(displayKey .. "#scaleX", 1)
                            local scaleY = xmlFile:getValue(displayKey .. "#scaleY", 1)

                            local emissiveScale = xmlFile:getValue(displayKey .. "#emissiveScale", 0.2)
                            local colour = xmlFile:getValue(displayKey .. "#color", {0.9, 0.9, 0.9, 1}, true)

                            display.characterLine = CharacterLine.new(display.displayNode, fontMaterial, mask:len())
                            display.characterLine:setSizeAndScale(size, scaleX, scaleY)
                            display.characterLine:setTextAlignment(alignment)
                            display.characterLine:setColor(colour, hiddenColour, emissiveScale)

                            if size >= 0.1 then
                                local characters = display.characterLine.characters

                                for i = 1, #characters do
                                    setClipDistance(characters[i], 150)
                                end
                            end

                            display:onFillLevelChange(0, 0, FillType.UNKNOWN)

                            local fillTypes, categoryType = getCategoryTypeFillTypes(xmlFile:getValue(displayKey .. "#categoryType", "convertedType"))

                            display.categoryType = categoryType

                            if fillTypes ~= nil then
                                for fillTypeIndex, _ in pairs(fillTypes) do
                                    if spec.displays[fillTypeIndex] == nil then
                                        spec.displays[fillTypeIndex] = {}
                                    end

                                    table.insert(spec.displays[fillTypeIndex], display)
                                end
                            end
                        end
                    end
                end
            end

            if next(spec.displays) == nil then
                spec.displays = nil
            end
        end

        spec.samples = {
            start = g_soundManager:loadSampleFromXML(xmlFile, "placeable.seedCleaner.sounds", "start", self.baseDirectory, self.components, 1, AudioGroup.ENVIRONMENT, self.i3dMappings, nil),
            stop = g_soundManager:loadSampleFromXML(xmlFile, "placeable.seedCleaner.sounds", "stop", self.baseDirectory, self.components, 1, AudioGroup.ENVIRONMENT, self.i3dMappings, nil),
            work = g_soundManager:loadSampleFromXML(xmlFile, "placeable.seedCleaner.sounds", "work", self.baseDirectory, self.components, 0, AudioGroup.ENVIRONMENT, self.i3dMappings, nil)
        }

        local objectChanges = {}

        ObjectChangeUtil.loadObjectChangeFromXML(xmlFile, "placeable.seedCleaner.objectChanges", objectChanges, components, self)

        if #objectChanges > 0 then
            ObjectChangeUtil.setObjectChanges(objectChanges, false)
            spec.objectChanges = objectChanges
        end

        spec.animationNodes = g_animationManager:loadAnimations(xmlFile, "placeable.seedCleaner.animationNodes", self.components, self, self.i3dMappings)

        spec.effects = g_effectManager:loadEffect(xmlFile, "placeable.seedCleaner.effectNodes", self.components, self, self.i3dMappings)
        g_effectManager:setEffectTypeInfo(spec.effects, FillType.UNKNOWN)
    end

    if not self.isServer then
        SpecializationUtil.removeEventListener(self, "onUpdate", PlaceableSeedCleaner) -- only used by server
    end
end

function PlaceableSeedCleaner:onFinalizePlacement()
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]
    local storageSystem = g_currentMission.storageSystem

    spec.unloadingStation:register(true)
    storageSystem:addUnloadingStation(spec.unloadingStation, self)

    spec.loadingStation:register(true)
    storageSystem:addLoadingStation(spec.loadingStation, self)

    spec.storage:setOwnerFarmId(self:getOwnerFarmId(), true)
    spec.storage:register(true)
    storageSystem:addStorage(spec.storage)
    storageSystem:addStorageToUnloadingStation(spec.storage, spec.unloadingStation)
    storageSystem:addStorageToLoadingStation(spec.storage, spec.loadingStation)

    if spec.interactionTrigger ~= nil then
        addTrigger(spec.interactionTrigger, "interactionTriggerCallback", self)
    end
end

function PlaceableSeedCleaner:onDelete()
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]
    local storageSystem = g_currentMission.storageSystem

    if spec.storage ~= nil then
        if spec.unloadingStation ~= nil then
            storageSystem:removeStorageFromUnloadingStations(spec.storage, {
                spec.unloadingStation
            })
        end

        if spec.loadingStation ~= nil then
            storageSystem:removeStorageFromLoadingStations(spec.storage, {
                spec.loadingStation
            })
        end

        storageSystem:removeStorage(spec.storage)
        spec.storage:delete()
        spec.storage = nil
    end

    if spec.unloadingStation ~= nil then
        storageSystem:removeUnloadingStation(spec.unloadingStation, self)
        spec.unloadingStation:delete()
        spec.unloadingStation = nil
    end

    if spec.loadingStation ~= nil then
        if spec.loadingStation:getIsFillTypeSupported(FillType.LIQUIDMANURE) then
            g_currentMission:removeLiquidManureLoadingStation(spec.loadingStation)
        end

        storageSystem:removeLoadingStation(spec.loadingStation, self)
        spec.loadingStation:delete()
        spec.loadingStation = nil
    end

    g_currentMission.activatableObjectsSystem:removeActivatable(spec.activatable)

    if spec.interactionTrigger ~= nil then
        removeTrigger(spec.interactionTrigger)
        spec.interactionTrigger = nil
    end

    if self.isClient then
        if spec.samples ~= nil then
            g_soundManager:deleteSamples(spec.samples)
            spec.samples = nil
        end

        if spec.animationNodes ~= nil then
            g_animationManager:deleteAnimations(spec.animationNodes)
        end

        if spec.effects ~= nil then
            g_effectManager:deleteEffects(spec.effects)
        end

        if spec.fillPlanes ~= nil then
            for _, fillPlanes in pairs(spec.fillPlanes) do
                for _, fillPlane in ipairs(fillPlanes) do
                    fillPlane:delete()
                end
            end

            spec.fillPlanes = nil
        end
    end
end

function PlaceableSeedCleaner:onReadStream(streamId, connection)
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

    local unloadingStationId = NetworkUtil.readNodeObjectId(streamId)

    spec.unloadingStation:readStream(streamId, connection)
    g_client:finishRegisterObject(spec.unloadingStation, unloadingStationId)

    local loadingStationId = NetworkUtil.readNodeObjectId(streamId)

    spec.loadingStation:readStream(streamId, connection)
    g_client:finishRegisterObject(spec.loadingStation, loadingStationId)

    local storageId = NetworkUtil.readNodeObjectId(streamId)

    spec.storage:readStream(streamId, connection)
    g_client:finishRegisterObject(spec.storage, storageId)

    self:setSeedCleanerState(streamReadBool(streamId), true)
end

function PlaceableSeedCleaner:onWriteStream(streamId, connection)
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(spec.unloadingStation))
    spec.unloadingStation:writeStream(streamId, connection)
    g_server:registerObjectInStream(connection, spec.unloadingStation)

    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(spec.loadingStation))
    spec.loadingStation:writeStream(streamId, connection)
    g_server:registerObjectInStream(connection, spec.loadingStation)

    NetworkUtil.writeNodeObjectId(streamId, NetworkUtil.getObjectId(spec.storage))
    spec.storage:writeStream(streamId, connection)
    g_server:registerObjectInStream(connection, spec.storage)

    streamWriteBool(streamId, spec.active)
end

function PlaceableSeedCleaner:onUpdate(dt)
    if self.isServer then
        local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

        if spec.active and spec.storage ~= nil then
            spec.updateTime = spec.updateTime + dt

            if spec.updateTime >= 500 then
                local rawFillType, rawFactor, rawFillLevel = nil, 1, -1
                local lastUpdateTime = spec.updateTime

                spec.updateTime = 0

                for _, rawMaterialFillType in ipairs(spec.sortedRawMaterialFillTypes) do
                    rawFillLevel = spec.storage:getFillLevel(rawMaterialFillType.index)

                    if rawFillLevel > 0 then
                        rawFillType = rawMaterialFillType.index
                        rawFactor = rawMaterialFillType.factor

                        break
                    end
                end

                local freeCapacity = spec.storage:getFreeCapacity(spec.convertedFillType)

                if freeCapacity > 0 and rawFillLevel > 0 then
                    if not spec.useRealTime then
                        lastUpdateTime *= g_currentMission:getEffectiveTimeScale()

                        if lastUpdateTime <= 0 then
                            self:raiseActive()

                            return -- wait for time scale to be increased above 0 again so machine does not turn off
                        end
                    end

                    local delta = math.min(math.min(spec.conversionPerMs * lastUpdateTime, rawFillLevel), spec.storage:getFillLevel(spec.treatmentFillType) / spec.treatmentUsage)

                    if delta > 0 then
                        delta = math.min(delta, freeCapacity)

                        spec.storage:setFillLevel(spec.storage:getFillLevel(rawFillType) - math.max(delta / rawFactor, 0.5), rawFillType)
                        spec.storage:setFillLevel(spec.storage:getFillLevel(spec.treatmentFillType) - (spec.treatmentUsage * delta), spec.treatmentFillType)
                        spec.storage:setFillLevel(spec.storage:getFillLevel(spec.convertedFillType) + delta, spec.convertedFillType)
                    else
                        self:setSeedCleanerState(false)
                    end
                else
                    self:setSeedCleanerState(false)
                end
            end

            self:raiseActive()
        end
    end
end

function PlaceableSeedCleaner:setSeedCleanerState(active, noEventSend)
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

    spec.updateTime = 0
    spec.active = Utils.getNoNil(active, false)

    PlaceableSeedCleanerStateEvent.sendEvent(self, spec.active, noEventSend)

    if self.isClient then
        if spec.active then
            if spec.samples ~= nil then
                g_soundManager:stopSample(spec.samples.start)
                g_soundManager:stopSample(spec.samples.work)
                g_soundManager:stopSample(spec.samples.stop)

                g_soundManager:playSample(spec.samples.start)
                g_soundManager:playSample(spec.samples.work, 0, spec.samples.start)
            end

            ObjectChangeUtil.setObjectChanges(spec.objectChanges, true)

            g_animationManager:startAnimations(spec.animationNodes)
            g_effectManager:startEffects(spec.effects)
        else
            if spec.samples ~= nil then
                g_soundManager:stopSample(spec.samples.start)
                g_soundManager:stopSample(spec.samples.work)
                g_soundManager:stopSample(spec.samples.stop)

                g_soundManager:playSample(spec.samples.stop)
            end

            ObjectChangeUtil.setObjectChanges(spec.objectChanges, false)

            g_animationManager:stopAnimations(spec.animationNodes)
            g_effectManager:stopEffects(spec.effects)
        end
    end

    self:raiseActive()
end

function PlaceableSeedCleaner:loadFromXMLFile(xmlFile, key)
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

    spec.storage:loadFromXMLFile(xmlFile, key)
end

function PlaceableSeedCleaner:saveToXMLFile(xmlFile, key, usedModNames)
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

    spec.storage:saveToXMLFile(xmlFile, key, usedModNames)
end

function PlaceableSeedCleaner:setOwnerFarmId(superFunc, farmId, noEventSend)
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]

    superFunc(self, farmId, noEventSend)

    if self.isServer and spec.storage ~= nil then
        spec.storage:setOwnerFarmId(farmId, true)
    end
end

function PlaceableSeedCleaner:updateInfo(superFunc, infoTable)
    superFunc(self, infoTable)

    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]
    local storage, factor = spec.storage, 0

    if storage ~= nil then
        local inputMaterialsEmpty = true
        local fillLevel, inputTitle = 0, nil

        table.insert(infoTable, spec.infoTableStorage)

        for _, rawMaterialFillType in ipairs (spec.sortedRawMaterialFillTypes) do
            fillLevel = storage:getFillLevel(rawMaterialFillType.index)

            if fillLevel > 0.1 then
                inputTitle = g_fillTypeManager:getFillTypeTitleByIndex(rawMaterialFillType.index)
                factor = rawMaterialFillType.factor
                inputMaterialsEmpty = false

                break
            end
        end

        table.insert(infoTable, {
            title = inputTitle or spec.texts.defaultInputTitle,
            text = string.format(spec.texts.litresFormat, math.floor(fillLevel))
        })

        fillLevel = storage:getFillLevel(spec.treatmentFillType)
        inputMaterialsEmpty = inputMaterialsEmpty or fillLevel < 0.1

        table.insert(infoTable, {
            title = g_fillTypeManager:getFillTypeTitleByIndex(spec.treatmentFillType) or "Unknown",
            text = string.format(spec.texts.litresFormat, math.floor(fillLevel))
        })

        table.insert(infoTable, {
            title = g_fillTypeManager:getFillTypeTitleByIndex(spec.convertedFillType) or "Unknown",
            text = string.format(spec.texts.litresFormat, math.floor(storage:getFillLevel(spec.convertedFillType)))
        })

        if spec.active then
            spec.infoTableState.text = spec.texts.active
        elseif storage:getFreeCapacity(spec.convertedFillType) == 0 then
            spec.infoTableState.text = spec.texts.outOfSpace
        elseif inputMaterialsEmpty then
            spec.infoTableState.text = spec.texts.materialsMissing
        else
            spec.infoTableState.text = spec.texts.inactive
        end
    else
        spec.infoTableState.text = spec.texts.inactive
    end

    if spec.infoTableShowConfiguration then
        table.insert(infoTable, spec.infoTableProductionState)
        table.insert(infoTable, spec.infoTableProduction)
    end

    table.insert(infoTable, spec.infoTableStatus)
    table.insert(infoTable, spec.infoTableState)
end

function PlaceableSeedCleaner:collectPickObjects(superFunc, node)
    local spec = self[PlaceableSeedCleaner.SPEC_TABLE_NAME]
    local foundNode = false

    for _, unloadTrigger in ipairs(spec.unloadingStation.unloadTriggers) do
        if node == unloadTrigger.exactFillRootNode then
            foundNode = true

            break
        end
    end

    if not foundNode then
        for _, loadTrigger in ipairs(spec.loadingStation.loadTriggers) do
            if node == loadTrigger.triggerNode then
                foundNode = true

                break
            end
        end
    end

    if not foundNode then
        superFunc(self, node)
    end
end

function PlaceableSeedCleaner.getDisplayFunction(displayType)
    if displayType == "PERCENT" then
        return function(display, fillLevel, capacity)
            local percent = capacity > 0 and (math.floor(fillLevel + 0.5) / capacity) or 0

            if percent ~= display.lastValue then
                local int, floatPart = math.modf(percent * 100)
                local value = string.format(display.formatStr, int, math.abs(math.floor((floatPart + 1e-06) * 10 ^ display.formatPrecision)))

                display.characterLine:setText(value)
                display.lastValue = percent
            end
        end, "PERCENT"
    end

    if displayType == "CAPACITY" then
        return function(display, _, capacity)
            if capacity ~= display.lastValue then
                local int, floatPart = math.modf(capacity)
                local value = string.format(display.formatStr, int, math.abs(math.floor((floatPart + 1e-06) * 10 ^ display.formatPrecision)))

                display.characterLine:setText(value)
                display.lastValue = capacity
            end
        end, "CAPACITY"
    end

    return function(display, fillLevel, capacity)
        if fillLevel ~= display.lastValue then
            local int, floatPart = math.modf(fillLevel)
            local value = string.format(display.formatStr, int, math.abs(math.floor((floatPart + 1e-06) * 10 ^ display.formatPrecision)))

            display.characterLine:setText(value)
            display.lastValue = fillLevel
        end
    end, "FILLLEVEL"
end


PlaceableSeedCleanerStateEvent = {}
local PlaceableSeedCleanerStateEvent_mt = Class(PlaceableSeedCleanerStateEvent, Event)

InitEventClass(PlaceableSeedCleanerStateEvent, "PlaceableSeedCleanerStateEvent")

function PlaceableSeedCleanerStateEvent.emptyNew()
    local self = Event.new(PlaceableSeedCleanerStateEvent_mt)

    return self
end

function PlaceableSeedCleanerStateEvent.new(placeable, active)
    local self = PlaceableSeedCleanerStateEvent.emptyNew()

    self.placeable = placeable
    self.active = active

    return self
end

function PlaceableSeedCleanerStateEvent:writeStream(streamId, connection)
    NetworkUtil.writeNodeObject(streamId, self.placeable)
    streamWriteBool(streamId, self.active)
end

function PlaceableSeedCleanerStateEvent:readStream(streamId, connection)
    self.placeable = NetworkUtil.readNodeObject(streamId)
    self.active = streamReadBool(streamId)

    self:run(connection)
end

function PlaceableSeedCleanerStateEvent:run(connection)
    if self.placeable ~= nil then
        self.placeable:setSeedCleanerState(self.active, true)
    end

    if not connection:getIsServer() then
        g_server:broadcastEvent(self, nil, connection, self.placeable)
    end
end

function PlaceableSeedCleanerStateEvent.sendEvent(placeable, active, noEventSend)
    if noEventSend == nil or noEventSend == false then
        if g_server ~= nil then
            g_server:broadcastEvent(PlaceableSeedCleanerStateEvent.new(placeable, active), nil, nil, placeable)
        else
            g_client:getServerConnection():sendEvent(PlaceableSeedCleanerStateEvent.new(placeable, active))
        end
    end
end
