---
-- OverloadingPipe
--
-- @author      nilBrain
-- @date        2025-11-16
--
-- Copyright (C) nilBrain | Philipp Jaeger, Confidential, All Rights Reserved.

OverloadingPipe = {}

local modName = g_currentModName
local modDirectory = g_currentModDirectory

OverloadingPipe.SPEC_NAME = modName .. ".overloadingPipe"
OverloadingPipe.SPEC_TABLE_NAME = "spec_" .. modName .. ".overloadingPipe"

OverloadingPipe.STATE_RANGE_EPSILON = 0.001
OverloadingPipe.FAP_HOSE_TYPE_SUFFIX = "FAP"


---Called while initializing the specialization
function OverloadingPipe.initSpecialization()
    local schema = Vehicle.xmlSchema

    schema:register(XMLValueType.NODE_INDEX, "vehicle.overloadingPipe.exactFillRootNode#node", "Exact fill root node")
    schema:register(XMLValueType.STRING, "vehicle.overloadingPipe.fillTypes#names", "Supported fill type names (space-separated)")
    schema:register(XMLValueType.STRING, "vehicle.overloadingPipe.fillTypes#categories", "Supported fill type categories (space-separated)")
    schema:register(XMLValueType.FLOAT, "vehicle.overloadingPipe.pipeStates.state(?)#rangeMin", "Lower Z-bound of the pipe state range")
    schema:register(XMLValueType.FLOAT, "vehicle.overloadingPipe.pipeStates.state(?)#rangeMax", "Upper Z-bound of the pipe state range")

    ObjectChangeUtil.registerObjectChangeXMLPaths(schema, "vehicle.overloadingPipe.pipeStates.state(?)")
end

---Register all functions from the specialization that can be called on vehicle level
-- @param table vehicleType vehicle type
function OverloadingPipe.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "getManureBarrel", OverloadingPipe.getManureBarrel)
    SpecializationUtil.registerFunction(vehicleType, "connectManureBarrel", OverloadingPipe.connectManureBarrel)
    SpecializationUtil.registerFunction(vehicleType, "disconnectFAPHosesToBarrel", OverloadingPipe.disconnectFAPHosesToBarrel)
    SpecializationUtil.registerFunction(vehicleType, "clearManureBarrelState", OverloadingPipe.clearManureBarrelState)
    SpecializationUtil.registerFunction(vehicleType, "setPipeSupportFramePosition", OverloadingPipe.setPipeSupportFramePosition)
end

---Register all events that should be called for this specialization
-- @param table vehicleType vehicle type
function OverloadingPipe.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", OverloadingPipe)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate", OverloadingPipe)
    SpecializationUtil.registerEventListener(vehicleType, "onDelete", OverloadingPipe)
    SpecializationUtil.registerEventListener(vehicleType, "onPreDetach", OverloadingPipe)
    SpecializationUtil.registerEventListener(vehicleType, "onStateChange", OverloadingPipe)
end

---Checks if all prerequisite specializations are loaded
-- @param table specializations specializations
-- @return boolean hasPrerequisite true if all prerequisite specializations are loaded
function OverloadingPipe.prerequisitesPresent(specializations)
    local hasPrerequisite = SpecializationUtil.hasSpecialization(Attachable, specializations)
    return hasPrerequisite
end

---Called on load
-- @param table savegame savegame
function OverloadingPipe:onLoad(savegame)
    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]

    spec.fillTypes = {}
    spec.manureBarrel = nil
    spec.manureBarrelFillUnit = nil

    local key = "vehicle.overloadingPipe"
    local exactFillRootNode = self.xmlFile:getNode(key .. ".exactFillRootNode#node", nil, self.components, self.i3dMappings)
    if exactFillRootNode == nil then
        Logging.xmlWarning(self.xmlFile, "ExactFillRootNode not found '%s'!", key)
        return
    end

    if not CollisionFlag.getHasGroupFlagSet(exactFillRootNode, CollisionFlag.FILLABLE) then
        Logging.xmlWarning(self.xmlFile, "Missing collision group %s. Please add this bit to exact fill root node '%s' collision filter group in '%s'", CollisionFlag.getBitAndName(CollisionFlag.FILLABLE), getName(exactFillRootNode), key)
        return
    end

    spec.exactFillRootNode = exactFillRootNode

    local fillTypeCategories = self.xmlFile:getString(key .. ".fillTypes#categories")
    local fillTypeNames = self.xmlFile:getString(key .. ".fillTypes#names")
    local fillTypes = g_fillTypeManager:getFillTypesByCategoryNames(fillTypeCategories, "Warning: '" .. tostring(key) .. "' has invalid fillTypeCategory '%s'.")
    fillTypes = g_fillTypeManager:getFillTypesByNames(fillTypeNames, "Warning: '" .. tostring(key) .. "' has invalid fillType '%s'.", fillTypes)

    for _, fillType in ipairs(fillTypes) do
        spec.fillTypes[fillType] = true
    end

    spec.pipeStates = {}
    for _, stateKey in self.xmlFile:iterator(key .. ".pipeStates.state") do
        local min = self.xmlFile:getFloat(stateKey .. "#rangeMin")
        local max = self.xmlFile:getFloat(stateKey .. "#rangeMax")

        if min == nil or max == nil then
            Logging.xmlWarning(self.xmlFile, "Missing rangeMin/rangeMax in '%s'", stateKey)
        end

        min = min or 0
        max = max or 0

        if max < min then
            min, max = max, min
        end

        local state = {
            min = min,
            max = max,
            objectChanges = {}
        }

        ObjectChangeUtil.loadObjectChangeFromXML(self.xmlFile, stateKey, state.objectChanges, self.components, self)
        ObjectChangeUtil.setObjectChanges(state.objectChanges, false, self)
        table.insert(spec.pipeStates, state)
    end
end

---Called on update
-- @param float dt time since last call in ms
function OverloadingPipe:onUpdate(dt)
    self:setPipeSupportFramePosition()

    if not self.isServer then
        return
    end

    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]

    if spec.manureBarrel == nil then
        local manureBarrel, manureBarrelFillUnit = self:getManureBarrel()
        if manureBarrel ~= nil and manureBarrelFillUnit ~= nil then
            self:connectManureBarrel(manureBarrel, manureBarrelFillUnit)
        end

        return
    end

    local rootVehicle = self:getRootVehicle()
    local barrel = spec.manureBarrel
    local barrelRootVehicle = nil

    if barrel.rootNode ~= nil and barrel.getRootVehicle ~= nil then
        barrelRootVehicle = barrel:getRootVehicle()
    end

    local hasDetached = rootVehicle == self or barrelRootVehicle == nil or barrelRootVehicle ~= rootVehicle
    if hasDetached then
        self:disconnectFAPHosesToBarrel(barrel)
        self:clearManureBarrelState()
    end
end

---Called on delete
function OverloadingPipe:onDelete()
    if not self.isServer then
        return
    end

    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]
    if spec.manureBarrel == nil then
        return
    end

    self:disconnectFAPHosesToBarrel(spec.manureBarrel)
    self:clearManureBarrelState()
end

---Called before detach
function OverloadingPipe:onPreDetach()
    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]
    self:disconnectFAPHosesToBarrel(spec.manureBarrel)
end

---Called when root vehicle changes
-- @param integer state state change id
-- @param Vehicle vehicle related vehicle
-- @param boolean isControlling whether the player is controlling
function OverloadingPipe:onStateChange(state, vehicle, isControlling)
    if state ~= VehicleStateChange.ATTACH and state ~= VehicleStateChange.DETACH then
        return
    end

    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]
    local manureBarrel, manureBarrelFillUnit = self:getManureBarrel()

    if spec.manureBarrel == nil and manureBarrel ~= nil and manureBarrelFillUnit ~= nil then
        self:connectManureBarrel(manureBarrel, manureBarrelFillUnit)
    elseif (spec.manureBarrel ~= nil and manureBarrel == nil) or (self:getRootVehicle() == self) then
        self:disconnectFAPHosesToBarrel(spec.manureBarrel)
        self:clearManureBarrelState()
    end

    self:setPipeSupportFramePosition()
end

---Connects the exact-fill mapping and FAP-typed hoses to the given manure barrel
-- @param Vehicle manureBarrel manure barrel vehicle
-- @param table manureBarrelFillUnit fill unit on the barrel
function OverloadingPipe:connectManureBarrel(manureBarrel, manureBarrelFillUnit)
    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]

    spec.manureBarrel = manureBarrel
    spec.manureBarrelFillUnit = manureBarrelFillUnit

    if spec.exactFillRootNode ~= nil then
        g_currentMission:addNodeObject(spec.exactFillRootNode, manureBarrel)
        manureBarrel.spec_fillUnit.exactFillRootNodeToFillUnit[spec.exactFillRootNode] = manureBarrelFillUnit
        manureBarrel.spec_fillUnit.exactFillRootNodeToExtraDistance[spec.exactFillRootNode] = 0
    end

    self:connectHosesToAttacherVehicle(manureBarrel, 1, 1)
    self:connectCustomHosesToAttacherVehicle(manureBarrel, 1, 1)
    manureBarrel:connectHosesToAttacherVehicle(self, 1, 1)
    manureBarrel:connectCustomHosesToAttacherVehicle(self, 1, 1)
end

---Disconnects all FAP-typed hoses between self and the barrel
-- @param Vehicle barrel manure barrel (may be nil)
function OverloadingPipe:disconnectFAPHosesToBarrel(barrel)
    local function disconnectActiveCustomFAPHoses(vehicle)
        if vehicle == nil or vehicle.spec_connectionHoses == nil or vehicle.disconnectCustomHoseNode == nil then
            return
        end

        local specConnectionHoses = vehicle.spec_connectionHoses
        if specConnectionHoses.customHoses ~= nil then
            for _, customHose in ipairs(specConnectionHoses.customHoses) do
                if customHose.isActive and customHose.connectedTarget ~= nil and string.endsWith(customHose.type or "", OverloadingPipe.FAP_HOSE_TYPE_SUFFIX) then
                    vehicle:disconnectCustomHoseNode(customHose, customHose.connectedTarget)
                end
            end
        end

        if specConnectionHoses.customHoseTargets ~= nil then
            for _, customTarget in ipairs(specConnectionHoses.customHoseTargets) do
                if customTarget.isActive and customTarget.connectedHose ~= nil and string.endsWith(customTarget.type or "", OverloadingPipe.FAP_HOSE_TYPE_SUFFIX) then
                    vehicle:disconnectCustomHoseNode(customTarget.connectedHose, customTarget)
                end
            end
        end
    end

    if barrel ~= nil and barrel.spec_connectionHoses ~= nil and barrel.spec_connectionHoses.connectionHoses ~= nil then
        for _, hose in ipairs(barrel.spec_connectionHoses.connectionHoses) do
            if string.endsWith(hose.type or "", OverloadingPipe.FAP_HOSE_TYPE_SUFFIX) then
                barrel:disconnectHose(hose)
            end
        end
    end

    if self.spec_connectionHoses ~= nil and self.spec_connectionHoses.connectionHoses ~= nil then
        for _, hose in ipairs(self.spec_connectionHoses.connectionHoses) do
            if string.endsWith(hose.type or "", OverloadingPipe.FAP_HOSE_TYPE_SUFFIX) then
                self:disconnectHose(hose)
            end
        end
    end

    disconnectActiveCustomFAPHoses(self)
    disconnectActiveCustomFAPHoses(barrel)
end

---Clears the cached manure barrel state and removes the exact fill mapping
function OverloadingPipe:clearManureBarrelState()
    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]

    if spec.exactFillRootNode ~= nil and spec.manureBarrel ~= nil and spec.manureBarrel.spec_fillUnit ~= nil then
        spec.manureBarrel.spec_fillUnit.exactFillRootNodeToFillUnit[spec.exactFillRootNode] = nil
        spec.manureBarrel.spec_fillUnit.exactFillRootNodeToExtraDistance[spec.exactFillRootNode] = nil
        g_currentMission:removeNodeObject(spec.exactFillRootNode)
    end

    spec.manureBarrel = nil
    spec.manureBarrelFillUnit = nil
end

---Finds the first child vehicle with a manure barrel spec and a supported fill unit
-- @return Vehicle|nil manureBarrel manure barrel vehicle
-- @return table|nil manureBarrelFillUnit fill unit on the barrel
function OverloadingPipe:getManureBarrel()
    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]

    local manureBarrel = nil
    local manureBarrelFillUnit = nil

    local rootVehicle = self:getRootVehicle()
    for _, childVehicle in ipairs(rootVehicle.childVehicles) do
        if childVehicle.spec_manureBarrel ~= nil then
            manureBarrel = childVehicle
            break
        end
    end

    if manureBarrel ~= nil then
        for _, fillUnit in ipairs(manureBarrel.spec_fillUnit.fillUnits) do
            for fillType in pairs(fillUnit.supportedFillTypes) do
                if spec.fillTypes[fillType] then
                    manureBarrelFillUnit = fillUnit
                    break
                end
            end

            if manureBarrelFillUnit ~= nil then
                break
            end
        end
    end

    return manureBarrel, manureBarrelFillUnit
end

---Updates the pipe support frame visibility based on its Z position
function OverloadingPipe:setPipeSupportFramePosition()
    local spec = self[OverloadingPipe.SPEC_TABLE_NAME]
    local supportFrameNode = nil
    local isFrameValid = false

    if self.getFAPSupportFrame ~= nil then
        supportFrameNode, isFrameValid = self:getFAPSupportFrame()
    end

    if supportFrameNode == nil then
        return
    end

    local _, _, z = getTranslation(supportFrameNode)

    local activeStateIndex = 0
    if isFrameValid then
        for stateIndex, state in ipairs(spec.pipeStates) do
            local rangeMin = math.min(state.min, state.max)
            local rangeMax = math.max(state.min, state.max)
            if (rangeMin - OverloadingPipe.STATE_RANGE_EPSILON) <= z and z <= (rangeMax + OverloadingPipe.STATE_RANGE_EPSILON) then
                activeStateIndex = stateIndex
                break
            end
        end
    end


    for stateIndex, state in ipairs(spec.pipeStates) do
        if stateIndex ~= activeStateIndex then
            ObjectChangeUtil.setObjectChanges(state.objectChanges, false, self)
        end
    end

    if activeStateIndex > 0 then
        ObjectChangeUtil.setObjectChanges(spec.pipeStates[activeStateIndex].objectChanges, true, self)
    end

    setVisibility(supportFrameNode, activeStateIndex > 0)
end
