----------------------------------------------------------------------------------------------------
-- SnowPlow
----------------------------------------------------------------------------------------------------
-- Purpose:  Adds the possibility to clear snow.
--
-- Copyright (c) Realismus Modding, 2019
----------------------------------------------------------------------------------------------------

---@class SnowPlow
SnowPlow = {}

SnowPlow.modName = g_currentModName
SnowPlow.MIN_SPEED = 0.4
SnowPlow.SPEED_LIMIT = 25
SnowPlow.EFFECT_TIME_THRESHOLD = 1000 --ms
SnowPlow.IMPACT_HEAP_THRESHOLD = 300 -- L

function SnowPlow.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(FillUnit, specializations)
end

function SnowPlow.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "getSnowPlowLine", SnowPlow.getSnowPlowLine)
    SpecializationUtil.registerFunction(vehicleType, "pickupSnow", SnowPlow.pickupSnow)
    SpecializationUtil.registerFunction(vehicleType, "dropSnow", SnowPlow.dropSnow)
    SpecializationUtil.registerFunction(vehicleType, "allowsSnowPlowing", SnowPlow.allowsSnowPlowing)
    SpecializationUtil.registerFunction(vehicleType, "loadSnowPlowNodeFromXML", SnowPlow.loadSnowPlowNodeFromXML)
    SpecializationUtil.registerFunction(vehicleType, "setSnowPlowEffectActive", SnowPlow.setSnowPlowEffectActive)
end

function SnowPlow.registerOverwrittenFunctions(vehicleType)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "updateWheelDensityMapHeight", SnowPlow.updateWheelDensityMapHeight)
    SpecializationUtil.registerOverwrittenFunction(vehicleType, "getPowerMultiplier", SnowPlow.getPowerMultiplier)
end

function SnowPlow.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", SnowPlow)
    SpecializationUtil.registerEventListener(vehicleType, "onDelete", SnowPlow)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdate", SnowPlow)
    SpecializationUtil.registerEventListener(vehicleType, "onReadUpdateStream", SnowPlow)
    SpecializationUtil.registerEventListener(vehicleType, "onWriteUpdateStream", SnowPlow)
    SpecializationUtil.registerEventListener(vehicleType, "onSetLowered", SnowPlow)
end

function SnowPlow:onLoad(savegame)
    self.spec_snowPlow = self["spec_" .. SnowPlow.modName .. ".snowPlow"]

    local spec = self.spec_snowPlow

    spec.snowPlowNodes = {}

    local i = 0
    while true do
        local key = ("vehicle.snowPlow.nodes.node(%d)"):format(i)
        if not hasXMLProperty(self.xmlFile, key) then
            break
        end

        local node = self:loadSnowPlowNodeFromXML(self.xmlFile, key)
        if node ~= nil then
            table.insert(spec.snowPlowNodes, node)
        end

        i = i + 1
    end

    spec.fillUnitIndex = Utils.getNoNil(getXMLFloat(self.xmlFile, "vehicle.snowPlow#fillUnitIndex"), 1)
    spec.pickUpDirection = Utils.getNoNil(getXMLInt(self.xmlFile, "vehicle.snowPlow#pickUpDirection"), 1)

    spec.snowAmountPowerMultiplier = 0
    spec.lastNoPickupTime = 0
    spec.isSnowPlowing = false
    spec.isSnowPlowingSent = false
    spec.dirtyFlag = self:getNextDirtyFlag()

    if not self:getFillUnitExists(spec.fillUnitIndex) then
        g_logManager:xmlWarning(self.configFileName, "Unknown fillUnitIndex '%s' for SnowPlow", tostring(spec.fillUnitIndex))
    end

    if self.isClient then
        spec.samples = {}
        spec.samples.work = g_soundManager:loadSampleFromXML(self.xmlFile, "vehicle.snowPlow.sounds", "work", self.baseDirectory, self.components, 0, AudioGroup.VEHICLE, self.i3dMappings, self)
        spec.isWorkSamplePlaying = false

        spec.effects = g_effectManager:loadEffect(self.xmlFile, "vehicle.snowPlow.effects", self.components, self, self.i3dMappings)
    end
end

function SnowPlow:onReadUpdateStream(streamId, timestamp, connection)
    if connection:getIsServer() then
        if streamReadBool(streamId) then
            local spec = self.spec_snowPlow
            spec.isSnowPlowing = streamReadBool(streamId)
            self:setSnowPlowEffectActive(spec.isSnowPlowing)
        end
    end
end

function SnowPlow:onWriteUpdateStream(streamId, connection, dirtyMask)
    if not connection:getIsServer() then
        local spec = self.spec_snowPlow

        if streamWriteBool(streamId, bitAND(dirtyMask, spec.dirtyFlag) ~= 0) then
            streamWriteBool(streamId, spec.isSnowPlowingSent)
        end
    end
end

function SnowPlow:onDelete()
    if self.isClient then
        local spec = self.spec_snowPlow
        g_soundManager:deleteSamples(spec.samples)
        g_effectManager:deleteEffects(spec.effects)
    end
end

function SnowPlow:loadSnowPlowNodeFromXML(xmlFile, key)
    local node = I3DUtil.indexToObject(self.components, getXMLString(xmlFile, key .. "#node"), self.i3dMappings)
    if node == nil then
        g_logManager:xmlWarning(self.configFileName, "Invalid note index for SnowPlow")
        return nil
    end

    local snowPlowNode = {}

    snowPlowNode.node = node
    snowPlowNode.width = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#width"), 1)

    snowPlowNode.rotDependentNode = I3DUtil.indexToObject(self.components, getXMLString(xmlFile, key .. "#rotDependentNode"), self.i3dMappings)
    snowPlowNode.rotThreshold = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#rotThreshold"), 0)

    snowPlowNode.maxDropWidth = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#maxDropWidth"), snowPlowNode.width)
    snowPlowNode.minDropWidth = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#minDropWidth"), snowPlowNode.width * 0.5)

    snowPlowNode.tipOffset = Utils.getNoNil(getXMLFloat(xmlFile, key .. "#tipOffset"), 1)

    snowPlowNode.lineOffsetPickUp = nil
    snowPlowNode.lineOffsetDrop = nil
    snowPlowNode.lastDrop = 0
    snowPlowNode.lastPickUp = 0

    return snowPlowNode
end

function SnowPlow:onUpdate(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local spec = self.spec_snowPlow

    if self.isClient then
        spec.isWorking = self:getIsLowered(true) and self:getLastSpeed() > SnowPlow.MIN_SPEED

        if spec.isWorking then
            if not spec.isWorkSamplePlaying then
                g_soundManager:playSample(spec.samples.work)
                spec.isWorkSamplePlaying = true
            end
        else
            if spec.isWorkSamplePlaying then
                g_soundManager:stopSample(spec.samples.work)
                spec.isWorkSamplePlaying = false
            end
        end
    end

    if not self.isServer then
        return
    end

    local heightType = g_densityMapHeightManager:getDensityMapHeightTypeByFillTypeIndex(FillType.SNOW)

    if heightType ~= nil then
        local isSnowPlowing = false
        local snowAmountPowerMultiplier = 0
        local currentTime = g_currentMission.time

        for _, node in pairs(spec.snowPlowNodes) do
            if self:allowsSnowPlowing() then
                local startPickupX, startPickupY, startPickupZ, endPickupX, endPickupY, endPickupZ = self:getSnowPlowLine(node, node.width, false)

                self:pickupSnow(node, startPickupX, startPickupY, startPickupZ, endPickupX, endPickupY, endPickupZ)
            end

            local fillLevelAfterPickup = self:getFillUnitFillLevel(spec.fillUnitIndex)
            if fillLevelAfterPickup > 0 then
                local capacity = self:getFillUnitCapacity(spec.fillUnitIndex)
                local factor = fillLevelAfterPickup / capacity
                local width = MathUtil.lerp(node.minDropWidth, node.maxDropWidth, factor)
                local startDropX, startDropY, startDropZ, endDropX, endDropY, endDropZ = self:getSnowPlowLine(node, width, true)

                self:dropSnow(node, startDropX, startDropY, startDropZ, endDropX, endDropY, endDropZ)

                isSnowPlowing = node.lastDrop ~= 0
                snowAmountPowerMultiplier = node.width * factor

                local lastPickUp = math.abs(node.lastPickUp)
                if lastPickUp >= SnowPlow.IMPACT_HEAP_THRESHOLD then
                    local pushFactor = (lastPickUp * 0.01) * 0.75
                    snowAmountPowerMultiplier = snowAmountPowerMultiplier + pushFactor
                end

                spec.lastNoPickupTime = currentTime
            end
        end

        if not isSnowPlowing then
            isSnowPlowing = (currentTime - spec.lastNoPickupTime) < SnowPlow.EFFECT_TIME_THRESHOLD
        end

        spec.isSnowPlowing = isSnowPlowing and self:getLastSpeed() > SnowPlow.MIN_SPEED
        spec.snowAmountPowerMultiplier = snowAmountPowerMultiplier

        if spec.isSnowPlowing ~= spec.isSnowPlowingSent then
            self:raiseDirtyFlags(spec.dirtyFlag)
            spec.isSnowPlowingSent = spec.isSnowPlowing
            self:setSnowPlowEffectActive(spec.isSnowPlowing)
        end
    end
end

---Empties the snowplow completely when lifted.
---@param lowered boolean
function SnowPlow:onSetLowered(lowered)
    if not lowered then
        local spec = self.spec_snowPlow
        if self.isServer then
            for _, node in pairs(spec.snowPlowNodes) do
                local startDropX, startDropY, startDropZ, endDropX, endDropY, endDropZ = self:getSnowPlowLine(node, node.width, true)
                self:dropSnow(node, startDropX, startDropY, startDropZ, endDropX, endDropY, endDropZ)
            end
        end

        if self.isClient then
            if spec.isWorkSamplePlaying then
                g_soundManager:stopSample(spec.samples.work)
                spec.isWorkSamplePlaying = false
            end
        end
    end
end

---Version of tipToGroundAroundLine that disables tipCollision so we can put snow anywhere it was picked up
function SnowPlow.adaptTipToGroundAroundLine(self, delta, startWorldX, startWorldY, startWorldZ, endWorldX, endWorldY, endWorldZ, innerRadius, outerRadius, offset, limitToLineHeight)
    local heightManager = g_densityMapHeightManager

    -- self.isServer is true in the shop
    if not g_currentMission:getIsServer() then
        return 0, 0
    end

    setDensityMapHeightCollisionMap(heightManager.terrainDetailHeightUpdater, 0, false)

    local dropped, lineOffset = DensityMapHeightUtil.tipToGroundAroundLine(
        self,
        delta,
        FillType.SNOW,
        startWorldX, startWorldY, startWorldZ,
        endWorldX, endWorldY, endWorldZ,
        innerRadius,
        outerRadius,
        offset, limitToLineHeight, nil)

    setDensityMapHeightCollisionMap(heightManager.terrainDetailHeightUpdater, heightManager.collisionMap, false)

    return dropped, lineOffset
end

---Returns true when allowed, false otherwise
function SnowPlow:allowsSnowPlowing()
    local spec = self.spec_snowPlow
    return spec.pickUpDirection == self.movingDirection
end

function SnowPlow:pickupSnow(node, startWorldX, startWorldY, startWorldZ, endWorldX, endWorldY, endWorldZ)
    local spec = self.spec_snowPlow

    local outerRadius = DensityMapHeightUtil.getDefaultMaxRadius(FillType.SNOW)
    local capacity = self:getFillUnitCapacity(spec.fillUnitIndex)
    local fillLevel = self:getFillUnitFillLevel(spec.fillUnitIndex)
    local delta = -(capacity - fillLevel)

    local lastPickUp, lineOffsetPickUp = SnowPlow.adaptTipToGroundAroundLine(
        self,
        delta,
        startWorldX, startWorldY - 0.1, startWorldZ,
        endWorldX, endWorldY - 0.1, endWorldZ,
        0,
        outerRadius,
        node.lineOffsetPickUp, true)

    node.lastPickUp = lastPickUp
    node.lineOffsetPickUp = lineOffsetPickUp

    local amountToFill = math.abs(lastPickUp)

    if amountToFill > 0 then
        self:addFillUnitFillLevel(self:getOwnerFarmId(), spec.fillUnitIndex, amountToFill, FillType.SNOW, ToolType.UNDEFINED, nil)
    end
end

function SnowPlow:dropSnow(node, startWorldX, startWorldY, startWorldZ, endWorldX, endWorldY, endWorldZ)
    local spec = self.spec_snowPlow
    local fillLevelAfterPickup = self:getFillUnitFillLevel(spec.fillUnitIndex)

    if fillLevelAfterPickup > 0 then
        local dropSowHeight = g_seasons.snowHandler.height * 2
        local outerRadius = DensityMapHeightUtil.getDefaultMaxRadius(FillType.SNOW)

        local lastDrop, lineOffsetDrop = SnowPlow.adaptTipToGroundAroundLine(
            self,
            fillLevelAfterPickup,
            startWorldX, startWorldY + dropSowHeight, startWorldZ,
            endWorldX, endWorldY + dropSowHeight, endWorldZ,
            0,
            outerRadius,
            node.lineOffsetDrop, false)

        node.lastDrop = lastDrop
        node.lineOffsetDrop = lineOffsetDrop

        if lastDrop > 0 then
            local leftOver = fillLevelAfterPickup - lastDrop
            if leftOver <= g_densityMapHeightManager:getMinValidLiterValue(FillType.SNOW) then
                node.lastDrop = fillLevelAfterPickup
            end
            self:addFillUnitFillLevel(self:getOwnerFarmId(), spec.fillUnitIndex, -node.lastDrop, FillType.SNOW, ToolType.UNDEFINED, nil)
        end
    end
end

---Gets the pickup or drop line for the given width.
---@param node table
---@param width number
---@param isDropLine boolean
function SnowPlow:getSnowPlowLine(node, width, isDropLine)
    local spec = self.spec_snowPlow
    local offsetWidth = 0.5 * width
    local _, rotY, _ = getRotation(node.rotDependentNode)
    local dir = 0
    if math.abs(math.deg(rotY)) > node.rotThreshold then
        dir = MathUtil.sign(rotY)
    end

    local tipOffset = node.tipOffset
    if isDropLine then
        local speedFactor = math.pow(math.max(1.0, spec.pickUpDirection * self:getLastSpeed() / SnowPlow.SPEED_LIMIT), 2)
        tipOffset = node.tipOffset + (node.tipOffset * width) * speedFactor
    end

    local x1, y1, z1 = localToWorld(node.node, -offsetWidth, 0, tipOffset)
    local x2, y2, z2 = localToWorld(node.node, offsetWidth, 0, tipOffset)
    local dx, _, dz = localDirectionToWorld(node.node, -dir, 0, 1)

    local isRight = dir < 1
    if isDropLine and dir ~= 0 then
        if isRight then
            x2 = x2 - dz * tipOffset
            z2 = z2 + dx * tipOffset
        else
            x1 = x1 + dz * tipOffset
            z1 = z1 - dx * tipOffset
        end
    end

    -- We invert the line because the tipping starts at the other side of the poly line.
    if not isRight then
        return x2, y2, z2, x1, y1, z1
    end

    return x1, y1, z1, x2, y2, z2
end

---Sets the power multiplier depending on the last speed.
function SnowPlow:getPowerMultiplier(superFunc)
    local powerMultiplier = superFunc(self)

    local spec = self.spec_snowPlow
    if spec.isSnowPlowing then
        local factor = 0
        local speedFactor = math.pow(math.max(1.0, self:getLastSpeed() / SnowPlow.SPEED_LIMIT), 2)
        for _, node in pairs(spec.snowPlowNodes) do
            factor = factor + node.tipOffset + (node.tipOffset * node.width) * speedFactor
        end

        return powerMultiplier * factor + spec.snowAmountPowerMultiplier
    end

    return powerMultiplier
end

---Disable smoothing to save some performance on the height map
function SnowPlow:updateWheelDensityMapHeight(superFunc, wheel, dt)
end

---Controls the effects
function SnowPlow:setSnowPlowEffectActive(isActive)
    if self.isClient then
        local spec = self.spec_snowPlow
        if isActive then
            g_effectManager:setFillType(spec.effects, FillType.SNOW)
            g_effectManager:startEffects(spec.effects)
        else
            g_effectManager:stopEffects(spec.effects)
        end
    end
end
