--[[
Interface: 1.6.0.0 b9173

Copyright (C) GtX (Andy), 2019

Author: GtX | Andy
Date: 12.09.2019
Version: 1.0.0.0

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

History:
V 1.0.0.0 @ 12.09.2019 - Release Version.
V 1.1.0.0 @ 05.01.2020 - Adjust detection code to fix random conflict on game end.
V 1.2.0.0 @ 08.07.2020 - Add support for 'IndividualSiloPlaceable' class, fixed GUI issue introduced by game changes made in update 1.6.0.0.

Important:
Free for use in other mods - no permission needed.
No changes are to be made to this script without permission from GtX | Andy

Frei verwendbar - keine erlaubnis nötig
An diesem Skript dürfen ohne Genehmigung von GtX | Andy keine Änderungen vorgenommen werden
]]


SiloDisplay = {}

local SiloDisplay_mt = Class(SiloDisplay, Placeable)
InitObjectClass(SiloDisplay, "SiloDisplay")

SiloDisplay.COLOURS = {
    RED = {1, 0, 0, 1},
    GREEN = {0, 1, 0, 1},
    WHITE = {0.8, 0.8, 0.8, 1},
    YELLOW = {1, 1, 0, 1},
    ORANGE = {0.9, 0.1, 0, 1},
    GREY = {0.3, 0.3, 0.3, 1},
    BLACK = {0.05, 0.05, 0.05, 1},
    LIGHT_RED = {1, 0.5, 0.25, 1},
    LIGHT_GREEN = {0.05, 0.15, 0.05, 1},
    DARK_GREEN = {0, 0.2, 0, 1}
}

SiloDisplay.HEADER_TEXTS = {
    fillType = "statistic_fillType",
    fillLevel = "info_fillLevel",
    capacity = "configuration_fillUnit",
    percent = "%"
}

function SiloDisplay:new(isServer, isClient, customMt)
    local self = Placeable:new(isServer, isClient, customMt or SiloDisplay_mt)

    registerObjectClassName(self, "SiloDisplay")

    self.storages = {}

    self.numStorages = 0
    self.stationsInRange = {}
    self.numStationsInRange = 0

    self.customIcons = {}
    self.displaySlots = {}
    self.fillTypeToSlot = {}

    self.userConfiguration = {
        active = false,
        fillTypes = {}
    }

    self.iconConvertor = {
        [FillType.DRYGRASS_WINDROW] = FillType.DRYGRASS,
        [FillType.GRASS_WINDROW] = FillType.GRASS
    }

    self.colours = {}

    self.maximumSlots = 0
    self.firstTimeRun = true

    self.playerInRange = false
    self.interactionTrigger = nil
    self.activateText = g_i18n:getText("button_configurate")

    self.hasFillTypeTexts = false
    self.guiWarningText = "Duplicate fruit types selected!"

    return self
end

function SiloDisplay:load(xmlFilename, x,y,z, rx,ry,rz, initRandom)
    if not SiloDisplay:superClass().load(self, xmlFilename, x,y,z, rx,ry,rz, initRandom) then
        return false
    end

    local key = "placeable.siloDisplay"
    local xmlFile = loadXMLFile("TempXML", xmlFilename)

    local i = 0
    while true do
        local directConnectionKey = string.format("%s.directConnections.directConnection(%d)", key, i)
        if not hasXMLProperty(xmlFile, directConnectionKey) then
            break
        end

        local displayLinkId = getXMLString(xmlFile, directConnectionKey .. "#displayLinkId")
        if displayLinkId ~= nil then
            if self.displayLinkIds == nil then
                self.displayLinkIds = {}
            end

            self.displayLinkIds[displayLinkId] = displayLinkId
        end

        i = i + 1
    end

    if self.displayLinkIds == nil then
        local triggerId = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, key .. ".interactionTrigger#node"))
        if triggerId ~= nil then
            addTrigger(triggerId, "interactionTriggerCallback", self)

            local activateText = getXMLString(xmlFile, key .. ".interactionTrigger#activateText")
            if activateText ~= nil and g_i18n:hasText(activateText, self.customEnvironment) then
                self.activateText = g_i18n:getText(activateText)
            end

            self.interactionTrigger = triggerId
        end

        if g_i18n:hasText("siloDisplay_guiWarning", self.customEnvironment) then
            self.guiWarningText = g_i18n:getText("siloDisplay_guiWarning")
        end
    end

    local defaultSeekRadius = Utils.getNoNil(getXMLInt(xmlFile, key .. ".setup#defaultSeekRadius"), 20)
    self.defaultSeekRadius = MathUtil.clamp(defaultSeekRadius, 10, 100)
    self.seekRadius = self.defaultSeekRadius

    self.textDrawDistance = Utils.getNoNil(getXMLInt(xmlFile, key .. ".setup#drawDistanceText3D"), 75)

    self.slotsRenderSize3D = Utils.getNoNil(getXMLFloat(xmlFile, key .. ".setup.slots.text3D#renderSize"), 0.08)
    self.slotsRenderWidth3D = Utils.getNoNil(getXMLFloat(xmlFile, key .. ".setup.slots.text3D#renderWidth"), 1) * 0.5

    -- Sets the horizontal alignment only. Vertical alignment is set to baseline at all times to allow easy positioning.
    local renderAlignment = Utils.getNoNil(getXMLString(xmlFile, key .. ".setup.slots.text3D#renderAlignment"), "ALIGN_CENTER")
    self.slotsRenderAlign3D = Utils.getNoNil(RenderText[renderAlignment], RenderText.ALIGN_CENTER)

    renderAlignment = Utils.getNoNil(getXMLString(xmlFile, key .. ".setup.headers#renderAlignment"), "ALIGN_CENTER")
    self.headersRenderAlign3D = Utils.getNoNil(RenderText[renderAlignment], RenderText.ALIGN_CENTER)

    self.colours.fillLevel = self:getDisplayColour(getXMLString(xmlFile, key .. ".setup.slots.colors#fillLevel"))
    self.colours.capacity = self:getDisplayColour(getXMLString(xmlFile, key .. ".setup.slots.colors#capacity"))
    self.colours.percent = self:getDisplayColour(getXMLString(xmlFile, key .. ".setup.slots.colors#percent"))
    self.colours.icon = Utils.getNoNil(self:getDisplayColour(getXMLString(xmlFile, key .. ".setup.slots.colors#icon")), SiloDisplay.COLOURS.WHITE)
    self.colours.title = Utils.getNoNil(self:getDisplayColour(getXMLString(xmlFile, key .. ".setup.slots.colors#title")), SiloDisplay.COLOURS.WHITE)
    self.colours.header = Utils.getNoNil(self:getDisplayColour(getXMLString(xmlFile, key .. ".setup.headers#color")), SiloDisplay.COLOURS.WHITE)

    self:loadHeader(xmlFile, key .. ".setup", "fillType")
    self:loadHeader(xmlFile, key .. ".setup", "fillLevel")
    self:loadHeader(xmlFile, key .. ".setup", "capacity")
    self:loadHeader(xmlFile, key .. ".setup", "percent")

    self.iconMaterialTypeName = string.upper(Utils.getNoNil(getXMLString(xmlFile, key .. ".setup.slots.icons#materialTypeName"), "SILO_DISPLAY_ICON"))
    self.iconMaterialParameter = Utils.getNoNil(getXMLString(xmlFile, key .. ".setup.slots.icons#materialShaderParameter"), "iconColor")
    self.iconW = Utils.getNoNil(getXMLInt(xmlFile, key .. ".setup.slots.icons#iconW"), 1)

    local backupIcon = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, key .. ".setup.slots.icons#backup"))
    if backupIcon ~= nil then
        if getHasClassId(backupIcon, ClassIds.SHAPE) then
            self.backupIcon = getMaterial(node, 0)
        else
            g_logManager:xmlWarning(xmlFilename, "Node is not a shape for '%s.setup.slots.icons#backupIcon'!", key)
        end
    end

    i = 0
    while true do
        local slotKey = string.format("%s.slots.slot(%d)", key, i)
        if not hasXMLProperty(xmlFile, slotKey) then
            break
        end

        local slot = {title = ""}

        slot.fillLevelDisplay = self:loadDisplayType(xmlFile, slotKey .. "#fillLevel", xmlFilename, false)
        slot.capacityDisplay = self:loadDisplayType(xmlFile, slotKey .. "#capacity", xmlFilename, false)
        slot.percentDisplay = self:loadDisplayType(xmlFile, slotKey .. "#percent", xmlFilename, false)

        local titleRenderNode = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, slotKey .. "#titleRenderNode"))
        slot.text = {node = titleRenderNode, x = 0, y = 0, z = 0, rx = 0, ry = 0, rz = 0}

        local iconNode = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, slotKey .. "#icon"))
        if iconNode ~= nil then
            local originalMaterial = getMaterial(iconNode, 0)
            setVisibility(iconNode, false)

            slot.icon = {node = iconNode, originalMaterial = originalMaterial}
        end

        table.insert(self.displaySlots, slot)

        i = i + 1
    end

    self.maximumSlots = #self.displaySlots

    delete(xmlFile)
    xmlFile = nil

    return true
end

function SiloDisplay:loadDisplayType(xmlFile, key, xmlFilename)
    local node = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, key))

    if node ~= nil then
        local numOfChildren = getNumOfChildren(node)
        if numOfChildren > 0 then
            local display = {node = node}

            display.maxValue = (10 ^ numOfChildren) - 1
            I3DUtil.setNumberShaderByValue(node, 0, 0, true)

            return display
        else
            g_logManager:xmlWarning(xmlFilename, "No children found at node! '%s'", key)
        end
    end

    return
end

function SiloDisplay:loadHeader(xmlFile, key, typeName)
    local headerKey = string.format("%s.headers.%s", key, typeName)

    local node = I3DUtil.indexToObject(self.nodeId, getXMLString(xmlFile, headerKey .. "#node"))
    if node ~= nil then
        if self.headers == nil then
            self.headers = {}
        end

        local text = getXMLString(xmlFile, headerKey .. "#text")
        if text == nil or not g_i18n:hasText(text, self.customEnvironment) then
            text = SiloDisplay.HEADER_TEXTS[typeName] or ""
        end

        local size = Utils.getNoNil(getXMLFloat(xmlFile, headerKey .. "#renderSize") , 0.08)

        if text ~= "" and text ~= "%" then
            local fullText = string.upper(g_i18n:getText(text))

            -- Correct this for English as 'Fruit Type' is not universal for all silo / storage types.
            if fullText == "FRUIT TYPE" then
                fullText = "FILL TYPE"
            end

            local maxWidth = Utils.getNoNil(getXMLFloat(xmlFile, headerKey .. "#renderWidth") , 1) * 0.55
            text = Utils.limitTextToWidth(fullText, size, maxWidth, false, "...")
        end

        self.headers[typeName] = {node = node, text = text, size = size}
    end
end

function SiloDisplay:finalizePlacement()
    SiloDisplay:superClass().finalizePlacement(self)

    if g_siloDisplays ~= nil then
        g_siloDisplays:addDisplay(self)
    end

    for i = 1, self.maximumSlots do
        local slot = self.displaySlots[i]

        self:setDisplayColour(slot.fillLevelDisplay, self.colours.fillLevel, false)
        self:setDisplayColour(slot.capacityDisplay, self.colours.capacity, false)
        self:setDisplayColour(slot.percentDisplay, self.colours.percent, false)

        if slot.text ~= nil and slot.text.node ~= nil then
            local x, y, z = getWorldTranslation(slot.text.node)
            slot.text.x = x
            slot.text.y = y
            slot.text.z = z

            local rx, ry, rz = getWorldRotation(slot.text.node)
            slot.text.rx = rx
            slot.text.ry = ry
            slot.text.rz = rz

            self.hasFillTypeTexts = true
        end
    end

    if self.hasFillTypeTexts and self.headers ~= nil then
        for _, header in pairs (self.headers) do
            local x, y, z = getWorldTranslation(header.node)
            header.x = x
            header.y = y
            header.z = z

            local rx, ry, rz = getWorldRotation(header.node)
            header.rx = rx
            header.ry = ry
            header.rz = rz
        end
    end

    self:raiseActive()
end

function SiloDisplay:delete()
    if self.interactionTrigger ~= nil then
        removeTrigger(self.interactionTrigger)
        self.interactionTrigger = nil
    end

    if self.playerInRange then
        self.playerInRange = false
        g_currentMission:removeActivatableObject(self)
    end

    for storage, _ in pairs (self.storages) do
        self:onStorageRemoved(storage, false)
    end

    if g_siloDisplays ~= nil then
        g_siloDisplays:removeDisplay(self)
    end

    unregisterObjectClassName(self)
    SiloDisplay:superClass().delete(self)
end

function SiloDisplay:readStream(streamId, connection)
    SiloDisplay:superClass().readStream(self, streamId, connection)

    if connection:getIsServer() then
        local seekRadius = streamReadUInt8(streamId)
        local userSet = streamReadBool(streamId)

        self.saveGameLoadData = {seekRadius = seekRadius, userSet = userSet}

        if userSet then
            self.saveGameLoadData.configuration = {}

            for i = 1, self.maximumSlots do
                local fillTypeIndex

                if streamReadBool(streamId) then
                    fillTypeIndex = streamReadUIntN(streamId, FillTypeManager.SEND_NUM_BITS)
                end

                self.saveGameLoadData.configuration[i] = {fillTypeIndex = fillTypeIndex}
            end
        end

        self.firstTimeRun = true
    end
end

function SiloDisplay:writeStream(streamId, connection)
    SiloDisplay:superClass().writeStream(self, streamId, connection)

    if not connection:getIsServer() then
        local seekRadius = self.seekRadius
        local userSet = self.userConfiguration.active
        local configuration = self.displaySlots

        if self.saveGameLoadData ~= nil then
            seekRadius = self.saveGameLoadData.seekRadius
            userSet = self.saveGameLoadData.userSet
            configuration = self.saveGameLoadData.configuration
        end

        if configuration == nil then
            userSet = false
        end

        streamWriteUInt8(streamId, seekRadius)
        streamWriteBool(streamId, userSet)

        if userSet then
            for i = 1, self.maximumSlots do
                if streamWriteBool(streamId, configuration[i].fillTypeIndex ~= nil) then
                    streamWriteUIntN(streamId, configuration[i].fillTypeIndex, FillTypeManager.SEND_NUM_BITS)
                end
            end
        end
    end
end

function SiloDisplay:loadFromXMLFile(xmlFile, key, resetVehicles)
    if not SiloDisplay:superClass().loadFromXMLFile(self, xmlFile, key, resetVehicles) then
        return false
    end

    if hasXMLProperty(xmlFile, key .. ".siloDisplay") then
        local seekRadius = Utils.getNoNil(getXMLInt(xmlFile, key .. ".siloDisplay#seekRadius"), self.defaultSeekRadius)
        local userSet = Utils.getNoNil(getXMLBool(xmlFile, key .. ".siloDisplay#userSet"), false)

        self.firstTimeRun = true
        self.saveGameLoadData = {seekRadius = seekRadius, userSet = userSet}

        if userSet then
            local addedFillTypes = {}
            self.saveGameLoadData.configuration = {}

            for i = 1, self.maximumSlots do
                local configKey = string.format("%s.siloDisplay.userConfiguration(%d)", key, i - 1)

                local fillTypeName = getXMLString(xmlFile, configKey .. "#fillType")
                local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)

                if fillTypeIndex ~= nil then
                    if addedFillTypes[fillTypeIndex] == nil then
                        addedFillTypes[fillTypeIndex] = i
                    else
                        fillTypeIndex = nil
                    end
                end

                self.saveGameLoadData.configuration[i] = {fillTypeIndex = fillTypeIndex}
            end

            addedFillTypes = nil
        end
    end

    return true
end

function SiloDisplay:saveToXMLFile(xmlFile, key, usedModNames)
    SiloDisplay:superClass().saveToXMLFile(self, xmlFile, key, usedModNames)

    setXMLInt(xmlFile, key .. ".siloDisplay#seekRadius", self.seekRadius)

    local userSet = self.userConfiguration.active
    setXMLBool(xmlFile, key .. ".siloDisplay#userSet", userSet)

    if userSet then
        for i = 1, self.maximumSlots do
            local configKey = string.format("%s.siloDisplay.userConfiguration(%d)", key, i - 1)

            setXMLInt(xmlFile, configKey .. "#slot", i)

            if self.displaySlots[i].fillTypeIndex ~= nil then
                local fillTypeName = g_fillTypeManager:getFillTypeNameByIndex(self.displaySlots[i].fillTypeIndex)
                setXMLString(xmlFile, configKey .. "#fillType", fillTypeName)
            end
        end
    end
end

function SiloDisplay:update(dt)
    if self.firstTimeRun then
        self.firstTimeRun = false

        if self.displayLinkIds == nil then
            if self.saveGameLoadData == nil then
                local seekRadius = 100
                local stationsInRange = self:getLoadingStationsInRange(false, self:getOwnerFarmId(), seekRadius)

                for _, station in pairs (stationsInRange) do
                    local distance = self:getDistanceFrom(self.nodeId, station.rootNode, nil)
                    if distance ~= nil then
                        distance = math.ceil(distance)

                        if distance < seekRadius then
                            seekRadius = distance
                        end
                    end
                end

                self.seekRadius = seekRadius
                self:onStorageAdded()
            else
                self:updateUserConfiguration(self.saveGameLoadData.seekRadius, self.saveGameLoadData.userSet, self.saveGameLoadData.configuration, true)
                self.saveGameLoadData = nil
            end
        end
    end

    if g_dedicatedServerInfo == nil and self.hasFillTypeTexts and self.numStorages > 0 then
        self:render3DTexts()
        self:raiseActive()
    end
end

function SiloDisplay:onStorageAdded()
    self:configureStorages()
    self:configureSlots()
end

function SiloDisplay:onStorageRemoved(storage, isStorageCall)
    if storage ~= nil then
        self.storages[storage] = nil

        if storage.siloDisplays ~= nil then
            storage.siloDisplays[self] = nil

            if next(storage.siloDisplays) == nil then
                storage.siloDisplays = nil
            end
        end
    end

    if isStorageCall and self.nodeId ~= nil then
        if self.nodeId ~= 0 and entityExists(self.nodeId) then
            self:configureStorages()
            self:configureSlots()
        end
    end
end

function SiloDisplay:hasStorage(storage)
    return self.storages[storage] ~= nil
end

function SiloDisplay:getIsStationInRange(station, checkDistance)
    if self.stationsInRange[station] ~= nil then
        return true
    end

    if self.displayLinkIds == nil then
        if checkDistance then
            local canAccessTarget = g_currentMission.accessHandler:canFarmAccess(self:getOwnerFarmId(), station)
            if canAccessTarget and not station.isBuyingPoint then
                local distance = self:getDistanceFrom(self.nodeId, station.rootNode, nil)

                if distance ~= nil then
                    return distance < self.seekRadius
                end
            end
        end
    else
        local siloDisplayLinkId = getUserAttribute(station.rootNode, "siloDisplayLinkId")
        if siloDisplayLinkId ~= nil then
            return self.displayLinkIds[siloDisplayLinkId] ~= nil
        end
    end

    return false
end

function SiloDisplay:configureStorages()
    local stationsInRange = self:getLoadingStationsInRange(false, self:getOwnerFarmId(), nil)

    self.numStationsInRange = #stationsInRange
    self.stationsInRange = {}

    local storages = {}
    local numStorages = 0

    if self.numStationsInRange > 0 then
        for i = 1, self.numStationsInRange do
            local station = stationsInRange[i]
            self.stationsInRange[station] = i

            for storage, _ in pairs (station.sourceStorages) do
                local farmId = self:getOwnerFarmId()
                if farmId == 0 or farmId == storage:getOwnerFarmId() then
                    if storage.fillLevels ~= nil and storages[storage] == nil then
                        storages[storage] = station
                        numStorages = numStorages + 1

                        if storage.siloDisplays == nil then
                            storage.siloDisplays = {}
                        end

                        -- Fix for 'IndividualSiloPlaceable' class as the 'setFillLevel' function does not call superFunc as it was not needed at the time.
                        if not storage.siloDisplayInjected then
                            storage.siloDisplayInjected = true

                            local owner = storage.storageOwner

                            if owner ~= nil and owner.vehiclesInRange ~= nil and owner.customEnvironment ~= nil then
                                if g_currentMission ~= nil and g_currentMission.objectsToClassName ~= nil then
                                    local className = g_currentMission.objectsToClassName[owner]
                                    local requiredClassName = string.format("%s.IndividualSiloPlaceable", owner.customEnvironment)

                                    if className ~= nil and className == requiredClassName then
                                        storage.setFillLevel = Utils.appendedFunction(storage.setFillLevel, function(individualSiloStorage, fillLevel, fillType)
                                            if individualSiloStorage.siloDisplays ~= nil then

                                                -- Reset the capacity displays when needed so only available capacity is displayed.
                                                if storage.currentStoredFillType ~= nil and storage.siloDisplayCapacityUpdated then
                                                    for siloDisplay, _ in pairs (individualSiloStorage.siloDisplays) do
                                                        siloDisplay:updateSlot(fillType, fillLevel, individualSiloStorage)
                                                    end
                                                else
                                                    for fillTypeIndex, fillLevelToSet in pairs(storage.fillLevels) do
                                                        for siloDisplay, _ in pairs (individualSiloStorage.siloDisplays) do
                                                            siloDisplay:updateSlot(fillTypeIndex, fillLevelToSet, individualSiloStorage)
                                                        end
                                                    end

                                                    storage.siloDisplayCapacityUpdated = fillLevel > 0
                                                end
                                            end
                                        end)
                                    end
                                end
                            end
                        end

                        storage.siloDisplays[self] = self
                    end
                end
            end
        end

        for storage, _ in pairs (self.storages) do
            if storages[storage] == nil then
                self:onStorageRemoved(storage, false)
            end
        end
    end

    self.storages = storages
    self.numStorages = numStorages
end

function SiloDisplay:configureSlots()
    local sortedFillTypes = {}
    local availableFillTypes = {}

    if self.numStorages > 0 then
        for storage, _ in pairs (self.storages) do
            for fillType, _ in pairs (storage.fillTypes) do
                if availableFillTypes[fillType] == nil and self:isValidFillType(fillType) then
                    availableFillTypes[fillType] = true

                    table.insert(sortedFillTypes, fillType)
                end
            end
        end
    end

    if #sortedFillTypes > 0 then
        table.sort(sortedFillTypes)

        self.fillTypeToSlot = {}

        for i = 1, self.maximumSlots do
            local fillTypeIndex
            local slot = self.displaySlots[i]

            if self.userConfiguration.active then
                fillTypeIndex = slot.fillTypeIndex

                if fillTypeIndex ~= nil and availableFillTypes[fillTypeIndex] == nil then
                    self.userConfiguration.fillTypes[fillTypeIndex] = nil
                    fillTypeIndex = nil
                end
            else
                fillTypeIndex = sortedFillTypes[i]
            end

            self:assignSlotInformation(slot, fillTypeIndex, fillTypeIndex ~= nil)
        end

    else
        for i = 1, self.maximumSlots do
            self:assignSlotInformation(self.displaySlots[i], nil, false)
        end
    end

    self:raiseActive()
end

function SiloDisplay:assignSlotInformation(slot, fillTypeIndex, isAdding)
    if slot ~= nil then
        local fillLevel = 0
        local capacity = 0

        if isAdding and fillTypeIndex ~= nil then

            local fillType = g_fillTypeManager:getFillTypeByIndex(fillTypeIndex)
            if fillType ~= nil then
                slot.title = Utils.limitTextToWidth(fillType.title, self.slotsRenderSize3D, self.slotsRenderWidth3D, false, "...")
            else
                slot.title = ""
            end

            slot.fillTypeIndex = fillTypeIndex
            self.fillTypeToSlot[fillTypeIndex] = slot

            for storage, _ in pairs (self.storages) do
                if storage:getIsFillTypeSupported(fillTypeIndex) then
                    fillLevel = fillLevel + storage.fillLevels[fillTypeIndex] or 0
                    capacity = capacity + storage.capacityPerFillType or 0
                end
            end
        else
            slot.title = ""
            slot.fillTypeIndex = nil

            if fillTypeIndex ~= nil then
                self.fillTypeToSlot[fillTypeIndex] = nil
            end
        end

        self:updateSlotIcon(slot, fillTypeIndex, isAdding)
        self:updateSlotDisplays(slot, fillLevel, capacity, isAdding)

        return true
    end

    return false
end

function SiloDisplay:updateSlot(fillTypeIndex, fillLevel, storage)
    local slot = self.fillTypeToSlot[fillTypeIndex]

    if slot ~= nil then
        local fillLevel = 0
        local capacity = 0

        for storage, _ in pairs (self.storages) do
            if storage:getIsFillTypeSupported(fillTypeIndex) then
                fillLevel = fillLevel + storage.fillLevels[fillTypeIndex] or 0
                capacity = capacity + storage.capacityPerFillType or 0
            end
        end

        self:updateSlotDisplays(slot, fillLevel, capacity, true)
    end
end

function SiloDisplay:updateSlotDisplays(slot, fillLevel, capacity, showNumbers)
    if slot.fillLevelDisplay ~= nil then
        I3DUtil.setNumberShaderByValue(slot.fillLevelDisplay.node, math.min(slot.fillLevelDisplay.maxValue, math.floor(math.max(0, fillLevel))), 0, showNumbers)
    end

    if slot.capacityDisplay ~= nil then
        I3DUtil.setNumberShaderByValue(slot.capacityDisplay.node, math.min(slot.capacityDisplay.maxValue, math.floor(math.max(0, capacity))), 0, showNumbers)
    end

    if slot.percentDisplay ~= nil then
        local percent = math.min(math.max(fillLevel / capacity, 0), 1)
        I3DUtil.setNumberShaderByValue(slot.percentDisplay.node, math.min(slot.percentDisplay.maxValue, math.abs(percent * 100)), 0, showNumbers)
    end
end

function SiloDisplay:render3DTexts()
    local x, z = 0, 0

    if self.worldTranslation == nil then
        self.worldTranslation = {getWorldTranslation(self.nodeId)}
    end

    if g_currentMission.controlPlayer then
        x, _, z = getWorldTranslation(g_currentMission.player.rootNode)
    elseif g_currentMission.controlledVehicle ~= nil then
        x, _, z = getWorldTranslation(g_currentMission.controlledVehicle.rootNode)
    end

    if MathUtil.vector2Length(x - self.worldTranslation[1], z - self.worldTranslation[3]) < self.textDrawDistance then
        setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_BASELINE)

        if self.headers ~= nil then
            setTextAlignment(self.headersRenderAlign3D)
            setTextColor(self.colours.header[1], self.colours.header[2], self.colours.header[3], 1.0)

            for _, header in pairs (self.headers) do
                renderText3D(header.x, header.y, header.z, header.rx, header.ry, header.rz, header.size, header.text)
            end
        end

        setTextAlignment(self.slotsRenderAlign3D)
        setTextColor(self.colours.title[1], self.colours.title[2], self.colours.title[3], 1.0)

        for i = 1, self.maximumSlots do
            local slot = self.displaySlots[i]
            renderText3D(slot.text.x, slot.text.y, slot.text.z, slot.text.rx, slot.text.ry, slot.text.rz, self.slotsRenderSize3D, slot.title)
        end

        -- Reset for scripts that forget to set alignment or colour each time.
        setTextColor(1.0, 1.0, 1.0, 1.0)
        setTextAlignment(RenderText.ALIGN_LEFT)
    end
end

function SiloDisplay:updateSlotIcon(slot, fillTypeIndex, isAdding)
    if slot.icon == nil then
        return
    end

    local material

    if fillTypeIndex ~= nil then
        if fillTypeIndex ~= FillType.UNKNOWN then
            if self.iconConvertor[fillTypeIndex] ~= nil then
                fillTypeIndex = self.iconConvertor[fillTypeIndex]
            end

            material = g_materialManager:getMaterial(fillTypeIndex, self.iconMaterialTypeName, 1)
        end

        if material == nil then
            material = self.backupIcon

            if material == nil then
                material = g_materialManager:getMaterial(FillType.UNKNOWN, self.iconMaterialTypeName, 1)
            end
        end

        if material ~= nil then
            setMaterial(slot.icon.node, material, 0)

            local iconW = self.iconW
            if iconW <= 0 then
                iconW = self.colours.icon[4] or 1
            end

            setShaderParameter(slot.icon.node, self.iconMaterialParameter, self.colours.icon[1], self.colours.icon[2], self.colours.icon[3], iconW, false)
        end
    end

    setVisibility(slot.icon.node, isAdding and material ~= nil)
end

function SiloDisplay:isValidFillType(fillTypeIndex)
    if self.userConfiguration.active then
        return self.userConfiguration.fillTypes[fillTypeIndex] ~= nil
    end

    return true
end

function SiloDisplay:getCanBePlacedAt(x, y, z, distance, farmId)
    local canBePlaced = SiloDisplay:superClass().getCanBePlacedAt(self, x, y, z, distance, farmId)

    if canBePlaced then
        if self.displayLinkIds == nil then
            local ox, oy, oz = getTranslation(self.nodeId)
            setTranslation(self.nodeId, x, y, z)

            local stationsInRange = self:getLoadingStationsInRange(true, farmId, nil)
            canBePlaced = #stationsInRange > 0

            setTranslation(self.nodeId, ox, oy, oz)
        else
            return true
        end
    end

    return canBePlaced
end

function SiloDisplay:getLoadingStationsInRange(isPlacement, farmId, seekRadius)
    local stationsInRange = {}
    local storageSystem = g_currentMission.storageSystem

    if storageSystem ~= nil then
        seekRadius = Utils.getNoNil(seekRadius, self.seekRadius)

        for station, _ in pairs(storageSystem.loadingStations) do
            if g_currentMission.accessHandler:canFarmAccess(farmId, station) and not station.isBuyingPoint then
                if station.sourceStorages ~= nil then
                    if self.displayLinkIds == nil then
                        local distance = self:getDistanceFrom(self.nodeId, station.rootNode, nil)
                        if distance ~= nil and distance < seekRadius then
                            table.insert(stationsInRange, station)

                            if isPlacement then
                                return stationsInRange
                            end
                        end
                    else
                        local siloDisplayLinkId = getUserAttribute(station.rootNode, "siloDisplayLinkId")
                        if siloDisplayLinkId ~= nil and self.displayLinkIds[siloDisplayLinkId] ~= nil then
                            table.insert(stationsInRange, station)
                        end
                    end
                end
            end
        end
    end

    return stationsInRange
end

function SiloDisplay:updateUserConfiguration(seekRadius, userSet, currentScreenLayout, noEventSend)
    seekRadius = math.max(Utils.getNoNil(seekRadius, self.defaultSeekRadius), 0)

    if userSet == nil or currentScreenLayout == nil or #currentScreenLayout == 0 then
        userSet = false
    end

    SiloDisplayEvent.sendEvent(self, seekRadius, userSet, currentScreenLayout, noEventSend)

    self.seekRadius = seekRadius
    self.userConfiguration.active = userSet
    self.userConfiguration.fillTypes = {}

    if userSet then
        for i = 1, self.maximumSlots do
            local fillTypeIndex

            if currentScreenLayout[i] ~= nil then
                fillTypeIndex = currentScreenLayout[i].fillTypeIndex
            end

            self.displaySlots[i].fillTypeIndex = fillTypeIndex

            if fillTypeIndex ~= nil then
                self.userConfiguration.fillTypes[fillTypeIndex] = i
            end
        end
    end

    self:onStorageAdded()
end

function SiloDisplay:interactionTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
    if onEnter or onLeave then
        if g_currentMission.player ~= nil and otherId == g_currentMission.player.rootNode then
            if onEnter then
                if g_currentMission.accessHandler:canFarmAccessOtherId(g_currentMission:getFarmId(), self:getOwnerFarmId()) then
                    if not self.playerInRange then
                        self.playerInRange = true

                        g_currentMission:addActivatableObject(self)
                    end
                end
            else
                if self.playerInRange then
                    self.playerInRange = false

                    g_currentMission:removeActivatableObject(self)
                end
            end

            self:raiseActive()
        end
    end
end

function SiloDisplay:getDisplayColour(colorStr)
    if colorStr == nil then
        return nil
    end

    local colorUpper = colorStr:upper()
    if SiloDisplay.COLOURS[colorUpper] ~= nil then
        return SiloDisplay.COLOURS[colorUpper]
    end

    local brandColor = g_brandColorManager:getBrandColorByName(colorStr)
    if brandColor ~= nil then
        return brandColor
    end

    local vector = StringUtil.getVectorNFromString(colorStr, 4)
    if vector ~= nil then
        return vector
    end
end

function SiloDisplay:setDisplayColour(display, rgb, shared)
    if display ~= nil and rgb ~= nil then
        if display.node ~= nil then
            shared = Utils.getNoNil(shared, false)

            for node, _ in pairs(I3DUtil.getNodesByShaderParam(display.node, "numberColor")) do
                setShaderParameter(node, "numberColor", rgb[1], rgb[2], rgb[3], 1, shared)
            end
        end
    end
end

function SiloDisplay:getDistanceFrom(node1, node2, backupValue)
    if (node1 ~= nil and node1 ~= 0) and (node2 ~= nil and node2 ~= 0) then
        return calcDistanceFrom(node1, node2)
    end

    return backupValue
end

function SiloDisplay:onActivateObject()
    local dialog = g_gui:showDialog("SiloDisplayGui")
    if dialog ~= nil then
        dialog.target:setTitle(g_i18n:getText("shop_configuration"))
        dialog.target:loadCallback(self.updateUserConfiguration, self)
        dialog.target:loadSlotData(self.displaySlots, self.seekRadius, self.guiWarningText)
    end
end

function SiloDisplay:drawActivate()
    return
end

function SiloDisplay:getIsActivatable()
    return g_currentMission.controlPlayer and (self:getOwnerFarmId() == 0 or g_currentMission:getFarmId() == self:getOwnerFarmId())
end

function SiloDisplay:shouldRemoveActivatable()
    return false
end
