--
-- AutomaticSectionControl
--
-- Author: Tobias Fricke
-- Description: Specialization for automatic section control
-- Name: AutomaticSectionControl
-- Hide: yes
--
-- Copyright (c) Creative Mesh, 2023

---@class AutomaticSectionControl
AutomaticSectionControl = {}
AutomaticSectionControl.MOD_NAME = g_currentModName
AutomaticSectionControl.AUTOMATIC_SECTIONS_UPDATE_PER_FRAME = 6

function AutomaticSectionControl.initSpecialization()
    local schema = Vehicle.xmlSchema

    schema:setXMLSpecializationType("VariableWorkWidth")

    for _, path in ipairs({
        "vehicle.variableWorkWidth",
        "vehicle.variableWorkWidth.variableWorkWidthConfigurations.variableWorkWidthConfiguration(?)"
    }) do

        schema:register(XMLValueType.BOOL, path .. ".sections#useWorkAreaPerSection", "WorkArea per section", false)
        schema:register(XMLValueType.BOOL, path .. ".sections.section(?)#isAutomatic", "Automatic section activation", true)
        schema:register(XMLValueType.INT, path .. ".sections.section(?)#fixedSortIndex", "Fixed sorting index", 0)
        schema:register(XMLValueType.FLOAT, path .. ".sections.section(?)#zOffset", "Automatic section detection offset in z direction", 1.0)
        schema:register(XMLValueType.TIME, path .. ".sections.section(?)#minActiveTime", "Minimum activation time of automatic section (sec)", 0.5)
        schema:register(XMLValueType.TIME, path .. ".sections.section(?)#minActiveTimePF", "Minimum activation time of automatic section if precision farming is loaded (sec)", 3.5)
        schema:register(XMLValueType.FLOAT, path .. ".sections.section(?)#testWidth", "Automatic section detection test width step", 1.0)
    end

    schema:setXMLSpecializationType()
end

function AutomaticSectionControl.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(VariableWorkWidth, specializations)
end

function AutomaticSectionControl.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "getVariableSections", AutomaticSectionControl.getVariableSections)
    SpecializationUtil.registerFunction(vehicleType, "updateAutomaticSectionEffect", AutomaticSectionControl.updateAutomaticSectionEffect)
    SpecializationUtil.registerFunction(vehicleType, "setSectionAutomaticMode", AutomaticSectionControl.setSectionAutomaticMode)
end

function AutomaticSectionControl.registerOverwrittenFunctions(vehicleType)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getVariableWorkWidth", AutomaticSectionControl.getVariableWorkWidth)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "updateSprayerEffects", AutomaticSectionControl.updateSprayerEffects)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getEffectByNode", AutomaticSectionControl.getEffectByNode)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "loadWorkAreaFromXML", AutomaticSectionControl.loadWorkAreaFromXML)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getWorkAreaWidth", AutomaticSectionControl.getWorkAreaWidth)
end

function AutomaticSectionControl.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onPreLoad", AutomaticSectionControl)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", AutomaticSectionControl)
    SpecializationUtil.registerEventListener(vehicleType, "onPostLoad", AutomaticSectionControl)
    SpecializationUtil.registerEventListener(vehicleType, "onReadUpdateStream", AutomaticSectionControl)
    SpecializationUtil.registerEventListener(vehicleType, "onWriteUpdateStream", AutomaticSectionControl)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate", AutomaticSectionControl)
	SpecializationUtil.registerEventListener(vehicleType, "onRegisterActionEvents", AutomaticSectionControl)
end

---Called before load
function AutomaticSectionControl:onPreLoad(savegame)
    self.spec_automaticSectionControl = self[string.format("spec_%s.automaticSectionControl", AutomaticSectionControl.MOD_NAME)]
    local spec = self.spec_automaticSectionControl

    spec.workAreaBySectionIndex = {}
end

---Called on load
function AutomaticSectionControl:onLoad(savegame)
    local spec = self.spec_automaticSectionControl

    spec.automaticMode = true
    spec.currentSectionUpdateIndex = 1

    spec.groundTypeMapId, spec.groundTypeFirstChannel, spec.groundTypeNumChannels = g_currentMission.fieldGroundSystem:getDensityMapData(FieldDensityMap.GROUND_TYPE)
    spec.sprayTypeMapId, spec.sprayTypeFirstChannel, spec.sprayTypeNumChannels = g_currentMission.fieldGroundSystem:getDensityMapData(FieldDensityMap.SPRAY_TYPE)

    spec.sectionsDirtyFlag = self:getNextDirtyFlag()
end

---Called after load
function AutomaticSectionControl:onPostLoad(savegame)
    local spec = self.spec_automaticSectionControl
    local configurationId = Utils.getNoNil(self.configurations.variableWorkWidth, 1)
    local configKey = string.format("vehicle.variableWorkWidth.variableWorkWidthConfigurations.variableWorkWidthConfiguration(%d)", configurationId - 1)

    if not self.xmlFile:hasProperty(configKey) then
        configKey = "vehicle.variableWorkWidth"
    end

    spec.useWorkAreaPerSection = self.xmlFile:getValue(configKey .. ".sections#useWorkAreaPerSection", false)

    self.xmlFile:iterate(configKey .. ".sections.section", function (index, key)
        -- sadly we have no save way to get the correct section without loading functions per section
        local section = self.spec_variableWorkWidth.sections[index]

        if section ~= nil then
            section.isAutomatic = self.xmlFile:getValue(key .. "#isAutomatic", true)
            section.fixedSortIndex = self.xmlFile:getValue(key .. "#fixedSortIndex", 0)
            section.zOffset = self.xmlFile:getValue(key .. "#zOffset", 1.0)

            if g_modIsLoaded["FS22_precisionFarming"] then
                section.minActiveTime = self.xmlFile:getValue(key .. "#minActiveTimePF", 3.5)
            else
                section.minActiveTime = self.xmlFile:getValue(key .. "#minActiveTime", 0.5)
            end

            section.testWidth = self.xmlFile:getValue(key .. "#testWidth", 1.0)
            section.lastActiveTime = 0
            section.isActiveSent = section.isActive
        end
    end)

	local function sort(a, b)
		return a.fixedSortIndex < b.fixedSortIndex
	end
	table.sort(self.spec_variableWorkWidth.sectionsLeft, sort)
	table.sort(self.spec_variableWorkWidth.sectionsRight, sort)

    if spec.automaticMode then
        SpecializationUtil.removeEventListener(self, "onRegisterActionEvents", VariableWorkWidth)

        self.spec_variableWorkWidth.drawInputHelp = true
    end
end

---Called on read update stream
function AutomaticSectionControl:onReadUpdateStream(streamId, timestamp, connection)
    if connection:getIsServer() then
        if streamReadBool(streamId) then
            local sections = self:getVariableSections()

            for _, section in ipairs(sections) do
                section.isActive = streamReadBool(streamId)
            end
        end
    end
end

---Called on write update stream
function AutomaticSectionControl:onWriteUpdateStream(streamId, connection, dirtyMask)
    if not connection:getIsServer() then
        local spec = self.spec_automaticSectionControl

        if streamWriteBool(streamId, bitAND(dirtyMask, spec.sectionsDirtyFlag) ~= 0) then
            local sections = self:getVariableSections()

            for _, section in ipairs(sections) do
                streamWriteBool(streamId, section.isActive)
            end
        end
    end
end

---Called on update
function AutomaticSectionControl:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local spec = self.spec_automaticSectionControl

    if not self.isServer or not self:getIsTurnedOn() or not spec.automaticMode then
        return
    end

    local anyDirtySection = false
    local sections = self:getVariableSections()

    for i = 1, AutomaticSectionControl.AUTOMATIC_SECTIONS_UPDATE_PER_FRAME, 1 do
        local section = sections[spec.currentSectionUpdateIndex]

        if section ~= nil and section.isAutomatic and section.lastActiveTime < g_currentMission.time then
            local workArea = spec.workAreaBySectionIndex[spec.currentSectionUpdateIndex]

            if workArea ~= nil then
                local isActive = false

                if self.lastSpeed * 1000 > 0.1 then
                    local workWidth = math.floor(workArea.workWidth)
                    local steps = math.floor(workWidth / section.testWidth)

                    local offsets = {}
                    for s = 0, steps, 1 do
                        table.insert(offsets, workArea.workWidth / steps * s)
                    end
                    table.insert(offsets, workArea.workWidth / steps * 0.5)
                    table.insert(offsets, workArea.workWidth / steps * (steps - 0.5))

                    for _, xOffset in ipairs(offsets) do
                        local x, y, z = localToWorld(workArea.start, -xOffset, 0, section.zOffset)
                        local densityBitsGround = getDensityAtWorldPos(spec.groundTypeMapId, x, y, z)
                        local groundType = bitAND(bitShiftRight(densityBitsGround, spec.groundTypeFirstChannel), 2^spec.groundTypeNumChannels - 1)

                        if self.spec_extendedSprayer == nil then
                            local densityBits = getDensityAtWorldPos(spec.sprayTypeMapId, x, y, z)
                            local sprayType = bitAND(bitShiftRight(densityBits, spec.sprayTypeFirstChannel), 2^spec.sprayTypeNumChannels - 1)

                            isActive = groundType ~= 0 and sprayType ~= FieldSprayType.FERTILIZER
                        else
                            local spec_extendedSprayer = self.spec_extendedSprayer
                            local isOnField, _ = FSDensityMapUtil.getFieldDataAtWorldPosition(x, 0, z)

                            if isOnField then
                                local sprayer, fillUnitIndex = FS22_precisionFarming.ExtendedSprayer.getFillTypeSourceVehicle(self)
                                local fillType = sprayer:getFillUnitLastValidFillType(fillUnitIndex)

                                if fillType == FillType.UNKNOWN then
                                    fillType = sprayer:getFillUnitFirstSupportedFillType(fillUnitIndex)
                                end

                                local nLevel = spec_extendedSprayer.nitrogenMap:getLevelAtWorldPos(x, z)
                                local nTarget = spec_extendedSprayer.nitrogenMap:getTargetLevelAtWorldPos(x, z, nil, nil, fillType, nLevel, spec_extendedSprayer.nApplyAutoModeFruitRequirementDefaultIndex)

                                isActive = groundType ~= 0 and nLevel < nTarget
                            end
                        end

                        if isActive then
                            break
                        end
                    end
                end

                if isActive ~= section.isActive then
                    section.isActive = isActive
                    anyDirtySection = true

                    if isActive then
                        section.lastActiveTime = g_currentMission.time + section.minActiveTime
                    end
                end
            end
        end

        spec.currentSectionUpdateIndex = spec.currentSectionUpdateIndex + 1
        if spec.currentSectionUpdateIndex > #sections then
            spec.currentSectionUpdateIndex = 1
        end
    end

    if anyDirtySection then
        self:raiseDirtyFlags(spec.sectionsDirtyFlag)
    end
end

---Returns table of sections
---@return table sections
function AutomaticSectionControl:getVariableSections()
    return self.spec_variableWorkWidth.sections
end

---Updates automatic section effect by section state
function AutomaticSectionControl:updateAutomaticSectionEffect()
    local spec = self.spec_automaticSectionControl

    if not self.isClient or not self:getIsTurnedOn() or not spec.useWorkAreaPerSection then
        return
    end

    local sections = self:getVariableSections()
    for _, section in ipairs(sections) do
        for _, effect in ipairs(section.effects) do
            local effectActive = effect.state == MotionPathEffect.STATE_ON or effect.state == MotionPathEffect.STATE_TURNING_ON

            if section.isActive and self:getAreEffectsVisible() then
                if not effectActive then
                    g_effectManager:startEffect(effect, true)
                end
            else
                if effectActive then
                    g_effectManager:stopEffect(effect, true)
                end
            end
        end
    end
end

---Sets automatic mode of section control
---@param isAutomatic boolean
function AutomaticSectionControl:setSectionAutomaticMode(isAutomatic, noEventSend)
    local spec = self.spec_automaticSectionControl

    if isAutomatic ~= nil and isAutomatic ~= spec.automaticMode then
        SectionAutomaticModeEvent.sendEvent(self, isAutomatic, noEventSend)

        spec.automaticMode = isAutomatic

        if isAutomatic then
            SpecializationUtil.removeEventListener(self, "onRegisterActionEvents", VariableWorkWidth)
            self.spec_variableWorkWidth.drawInputHelp = true
        else
            SpecializationUtil.registerEventListener(self, "onRegisterActionEvents", VariableWorkWidth)
        end

        self:requestActionEventUpdate()
    end
end

------------------
---ActionEvents---
------------------

function AutomaticSectionControl:onRegisterActionEvents(isActiveForInput, isActiveForInputIgnoreSelection)
	if self.isClient then
        local spec = self.spec_automaticSectionControl

		self:clearActionEventsTable(spec.actionEvents)

		if isActiveForInputIgnoreSelection then
			local _, actionEventId = self:addActionEvent(spec.actionEvents, InputAction.RA_TOGGLE_SECTION_AUTOMATIC, self, AutomaticSectionControl.actionEventToggleAutomaticMode, false, true, false, true, nil)
			g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_NORMAL)
		end
	end
end

function AutomaticSectionControl:actionEventToggleAutomaticMode(actionName, inputValue, callbackState, isAnalog)
    local spec = self.spec_automaticSectionControl
    self:setSectionAutomaticMode(not spec.automaticMode)
end

----------------
---Overwrites---
----------------

---Overwritten function: getVariableWorkWidth
---@param superFunc function Original function
---@param isLeft boolean is left section
---@return integer currentWidth
---@return integer maxWidth
---@return boolean isValid
function AutomaticSectionControl:getVariableWorkWidth(superFunc, isLeft)
    local spec = self.spec_automaticSectionControl
    if spec.useWorkAreaPerSection then
        return 36, 36, false
    end

    return superFunc(self, isLeft)
end

---Overwritten function: updateSprayerEffects
---@param superFunc function Original function
---@param force boolean is forced updated
function AutomaticSectionControl:updateSprayerEffects(superFunc, force)
    superFunc(self, force)

    self:updateAutomaticSectionEffect()
end

---Overwritten function: updateSectionStates
---@param superFunc function Original function
---@param sections table sections to update
---@param state boolean section state
function AutomaticSectionControl:updateSectionStates(superFunc, sections, state)
    superFunc(self, sections, state)

    self:updateAutomaticSectionEffect()
end

---Overwritten function: getEffectByNode
---@param superFunc function Original function
---@param node integer effect node
---@return Effect effect
function AutomaticSectionControl:getEffectByNode(superFunc, node)
    local spec = self.spec_sprayer

    --get effect by link node
    for _, effect in ipairs(spec.effects) do
        if node == effect.linkNode then
            return effect
        end
    end

    return superFunc(self, node)
end

---Overwritten function: loadWorkAreaFromXML
---@param superFunc function Original function
---@param workArea table work area
---@param xmlFile XMLFile xml file to load workArea from
---@param key string xml key
---@return any
function AutomaticSectionControl:loadWorkAreaFromXML(superFunc, workArea, xmlFile, key)
    local spec = self.spec_automaticSectionControl
    local retVal = superFunc(self, workArea, xmlFile, key)

    if workArea.sectionIndex ~= nil then
        spec.workAreaBySectionIndex[workArea.sectionIndex] = workArea
    end

    return retVal
end

---Overwritten function: getWorkAreaWidth
---@param superFunc function Original function
---@param workAreaIndex integer work area index
---@return integer workingWidth
function AutomaticSectionControl:getWorkAreaWidth(superFunc, workAreaIndex)
    local spec = self.spec_automaticSectionControl

    if not spec.useWorkAreaPerSection then
        return superFunc(self, workAreaIndex)
    end

    --we ignore the workarea index and return the width of all active sections
    local workingWidth = 0
    local sections = self:getVariableSections()

    for index, section in ipairs(sections) do
        local workArea = spec.workAreaBySectionIndex[index]

        if section.isActive then
            workingWidth = workingWidth + workArea.workWidth
        end
    end

    return workingWidth
end

---Precision farming injections, sad not the vanilla way to overwrite functions
if g_modIsLoaded["FS22_precisionFarming"] then
    local extendedSprayer = FS22_precisionFarming.ExtendedSprayer

    ---Overwritten function: updateSprayerEffectState
    ---@param force boolean is forced updated
    local function updateSprayerEffectState(self, force)
        if self.updateAutomaticSectionEffect ~= nil then
            self:updateAutomaticSectionEffect()
        end
    end

    extendedSprayer.updateSprayerEffectState = Utils.appendedFunction(extendedSprayer.updateSprayerEffectState, updateSprayerEffectState)
end
