DiseaseProtectionGrid = {}

DiseaseProtectionGrid.PEST_BIT = 1
DiseaseProtectionGrid.DISEASE_BIT = 2

local function clamp(v, a, b)
    return math.max(a, math.min(b, v))
end

local function buildSavePath()
    local info = g_currentMission and g_currentMission.missionInfo
    local dir = info and info.savegameDirectory or nil
    return dir ~= nil and dir ~= "" and (dir .. "/cropProtectionMask.grle") or nil
end

local function mapToWorld(width, height, mapX, mapZ)
    local terrainSize = g_currentMission and g_currentMission.terrainSize or 2048
    return (mapX + 0.5) / width * terrainSize - terrainSize * 0.5,
        (mapZ + 0.5) / height * terrainSize - terrainSize * 0.5
end

local function getDebugWorldPosition()
    local mission = g_currentMission
    if mission == nil then return nil, nil, nil end
    local player = g_localPlayer or mission.player
    if player ~= nil and player.rootNode ~= nil then return getWorldTranslation(player.rootNode) end
    local vehicle = mission.controlledVehicle
    if vehicle ~= nil then
        local node = vehicle.rootNode or (vehicle.components and vehicle.components[1] and vehicle.components[1].node)
        if node ~= nil then return getWorldTranslation(node) end
    end
    local object = mission.controlledObject
    if object ~= nil and object.rootNode ~= nil then return getWorldTranslation(object.rootNode) end
    if g_cameraManager ~= nil and g_cameraManager.getActiveCamera ~= nil then
        local camera = g_cameraManager:getActiveCamera()
        local node = camera and (camera.cameraNode or camera.rootNode or camera)
        if node ~= nil then return getWorldTranslation(node) end
    end
    return nil, nil, nil
end

local function getProtectionLabel(value)
    local pest = g_i18n ~= nil and g_i18n:getText("bmp_mapLayerPests") or "PEST"
    local disease = g_i18n ~= nil and g_i18n:getText("bmp_mapLayerDiseases") or "DISEASE"
    if pest == nil or pest == "" then pest = "PEST" end
    if disease == nil or disease == "" then disease = "DISEASE" end
    if value == 1 then return pest end
    if value == 2 then return disease end
    if value == 3 then return pest .. " + " .. disease end
    return ""
end

local function getFieldPolygon2D(field)
    if field == nil then return nil end
    local polygon = Polygon2D.new()
    local densityPolygon = field.getDensityMapPolygon ~= nil and field:getDensityMapPolygon() or nil
    local vertices = densityPolygon ~= nil and densityPolygon.getVerticesList ~= nil and densityPolygon:getVerticesList() or nil
    if vertices ~= nil and #vertices >= 6 then
        for i = 1, #vertices, 2 do polygon:addPos(vertices[i], vertices[i + 1]) end
    else
        for _, point in ipairs(field:getPolygonPoints() or {}) do
            local x, _, z = getWorldTranslation(point)
            polygon:addPos(x, z)
        end
    end
    return polygon:getNumVertices() >= 3 and polygon or nil
end

local function getFieldWorldBounds(field)
    local minX, minZ, maxX, maxZ = math.huge, math.huge, -math.huge, -math.huge
    local densityPolygon = field ~= nil and field.getDensityMapPolygon ~= nil and field:getDensityMapPolygon() or nil
    local vertices = densityPolygon ~= nil and densityPolygon.getVerticesList ~= nil and densityPolygon:getVerticesList() or nil
    if vertices ~= nil and #vertices >= 2 then
        for i = 1, #vertices, 2 do
            minX, maxX = math.min(minX, vertices[i]), math.max(maxX, vertices[i])
            minZ, maxZ = math.min(minZ, vertices[i + 1]), math.max(maxZ, vertices[i + 1])
        end
    else
        for _, point in ipairs(field and field:getPolygonPoints() or {}) do
            local x, _, z = getWorldTranslation(point)
            minX, maxX = math.min(minX, x), math.max(maxX, x)
            minZ, maxZ = math.min(minZ, z), math.max(maxZ, z)
        end
    end
    if minX == math.huge then return nil end
    return minX, minZ, maxX, maxZ
end

local function categoryBit(category)
    if category == "pest" then return DiseaseProtectionGrid.PEST_BIT end
    if category == "disease" then return DiseaseProtectionGrid.DISEASE_BIT end
    return 0
end

function DiseaseProtectionGrid:loadMap()
    self.savePath = g_server ~= nil and buildSavePath() or nil
    self.mapId = createBitVectorMap("CropProtectionGrid")
    local loaded = g_server ~= nil and self.savePath ~= nil and fileExists(self.savePath)
        and loadBitVectorMapFromFile(self.mapId, self.savePath, 2)
    local expectedWidth = DiseaseGrid ~= nil and tonumber(DiseaseGrid.width or 1024) or 1024
    local expectedHeight = DiseaseGrid ~= nil and tonumber(DiseaseGrid.height or 1024) or 1024
    if not loaded then
        loadBitVectorMapNew(self.mapId, expectedWidth, expectedHeight, 2, false)
    end
    local width, height = getBitVectorMapSize(self.mapId)
    self.width = tonumber(width or 1024) or 1024
    self.height = tonumber(height or 1024) or 1024
    local rebuilt = false
    if self.width ~= expectedWidth or self.height ~= expectedHeight then
        delete(self.mapId)
        self.mapId = createBitVectorMap("CropProtectionGridRebuilt")
        loadBitVectorMapNew(self.mapId, expectedWidth, expectedHeight, 2, false)
        self.width, self.height = expectedWidth, expectedHeight
        rebuilt = true
    end
    self.isDirty = rebuilt and g_server ~= nil
    self.saveTimer = 0
    self.saveIntervalMs = 15000
    self.debugEnabled = false
    self.debugRadius = 55
    addConsoleCommand("gsDiseaseProtectionDebug", "Toggle crop protection debug overlay: gsDiseaseProtectionDebug [0|1] [radiusM]", "consoleToggleDebug", self)
end

function DiseaseProtectionGrid:deleteMap()
    removeConsoleCommand("gsDiseaseProtectionDebug")
    if self.mapId ~= nil then
        self:saveNow(true)
        delete(self.mapId)
    end
    self.mapId = nil
end

function DiseaseProtectionGrid:consoleToggleDebug(enabledStr, radiusStr)
    if enabledStr == nil or enabledStr == "" then
        self.debugEnabled = not self.debugEnabled
    else
        self.debugEnabled = tonumber(enabledStr) == 1
    end
    local radius = tonumber(radiusStr or "")
    if radius ~= nil then self.debugRadius = math.max(10, math.min(radius, 200)) end
    print(string.format("Disease protection debug overlay: %s (radius=%dm)", tostring(self.debugEnabled), self.debugRadius or 55))
end

function DiseaseProtectionGrid:saveNow(force)
    if g_server == nil or self.mapId == nil or self.savePath == nil or (not force and not self.isDirty) then
        return false
    end
    saveBitVectorMapToFile(self.mapId, self.savePath)
    self.isDirty = false
    self.saveTimer = 0
    return true
end

function DiseaseProtectionGrid:update(dt)
    if g_server == nil or not self.isDirty then return end
    self.saveTimer = (self.saveTimer or 0) + (dt or 0)
    if self.saveTimer >= (self.saveIntervalMs or 15000) then self:saveNow(false) end
end

function DiseaseProtectionGrid:draw()
    if not self.debugEnabled or self.mapId == nil then return end
    local px, _, pz = getDebugWorldPosition()
    if px == nil then return end
    local terrainSize = g_currentMission and g_currentMission.terrainSize or 2048
    local cellSize = terrainSize / math.max(self.width or 1, 1)
    local radius = self.debugRadius or 55
    local startX, startZ = DiseaseGrid:worldToLocal(px - radius, pz - radius)
    local endX, endZ = DiseaseGrid:worldToLocal(px + radius, pz + radius)
    for z = startZ, endZ do
        for x = startX, endX do
            local value = self:getValue(x, z)
            if value > 0 then
                local cx, cz = mapToWorld(self.width, self.height, x, z)
                local y = getTerrainHeightAtWorldPos(g_terrainNode, cx, 0, cz) + 0.16
                local half = cellSize * 0.47
                local r, g, b = 0.10, 0.85, 1.00
                if value == 2 then r, g, b = 0.90, 0.20, 1.00 end
                if value == 3 then r, g, b = 1.00, 1.00, 1.00 end
                drawDebugLine(cx - half, y, cz - half, r, g, b, cx + half, y, cz - half, r, g, b)
                drawDebugLine(cx + half, y, cz - half, r, g, b, cx + half, y, cz + half, r, g, b)
                drawDebugLine(cx + half, y, cz + half, r, g, b, cx - half, y, cz + half, r, g, b)
                drawDebugLine(cx - half, y, cz + half, r, g, b, cx - half, y, cz - half, r, g, b)
                if math.abs(cx - px) <= cellSize * 2.0 and math.abs(cz - pz) <= cellSize * 2.0 then
                    Utils.renderTextAtWorldPosition(cx, y + 0.13, cz, getProtectionLabel(value), 0.011, 0, r, g, b, 1)
                end
            end
        end
    end
end

function DiseaseProtectionGrid:getValue(x, z)
    if self.mapId == nil then return 0 end
    return getBitVectorMapPoint(self.mapId, x, z, 0, 2) or 0
end

function DiseaseProtectionGrid:getValueAtWorldPos(worldX, worldZ)
    if self.mapId == nil or DiseaseGrid == nil or worldX == nil or worldZ == nil then return 0 end
    local x, z = DiseaseGrid:worldToLocal(worldX, worldZ)
    return self:getValue(x, z)
end

function DiseaseProtectionGrid:isProtectedCell(x, z, category)
    local bit = categoryBit(category)
    local value = self:getValue(x, z)
    return bit > 0 and value % (bit * 2) >= bit
end

function DiseaseProtectionGrid:setCellValue(x, z, value, suppressNetwork)
    if self.mapId == nil then return false end
    value = clamp(tonumber(value or 0) or 0, 0, 3)
    if self:getValue(x, z) == value then return false end
    if setBitVectorMapPoint ~= nil then
        setBitVectorMapPoint(self.mapId, x, z, 0, 2, value)
    else
        -- FS25 runtime builds do not all expose the point setter. A 1x1
        -- parallelogram is the engine-supported equivalent used by InfoLayer.
        setBitVectorMapParallelogram(self.mapId, x, z, 1, 0, 0, 1, 0, 2, value)
    end
    if self:getValue(x, z) ~= value then return false end
    self.isDirty = true
    if suppressNetwork ~= true and g_server ~= nil and DiseaseGrid ~= nil and DiseaseGrid.recordChangedCell ~= nil then
        local severity = DiseaseGrid.mapId ~= nil and (getBitVectorMapPoint(DiseaseGrid.mapId, x, z, 0, 2) or 0) or 0
        local threatId = DiseaseGrid.mapId ~= nil and (getBitVectorMapPoint(DiseaseGrid.mapId, x, z, 2, 4) or 0) or 0
        DiseaseGrid:recordChangedCell(x, z, severity, threatId, value)
    end
    return true
end

function DiseaseProtectionGrid:addProtectionCell(x, z, category)
    local bit = categoryBit(category)
    if bit <= 0 then return false end
    if self:isProtectedCell(x, z, category) then return false end

    -- Write only the category's physical channel. Fungicide can therefore
    -- never modify the pest bit, and insecticide can never modify disease.
    local firstChannel = category == "pest" and 0 or 1
    if setBitVectorMapPoint ~= nil then
        setBitVectorMapPoint(self.mapId, x, z, firstChannel, 1, 1)
    else
        setBitVectorMapParallelogram(self.mapId, x, z, 1, 0, 0, 1, firstChannel, 1, 1)
    end
    if not self:isProtectedCell(x, z, category) then return false end

    self.isDirty = true
    local value = self:getValue(x, z)
    if g_server ~= nil and DiseaseGrid ~= nil and DiseaseGrid.recordChangedCell ~= nil then
        local severity = DiseaseGrid.mapId ~= nil and (getBitVectorMapPoint(DiseaseGrid.mapId, x, z, 0, 2) or 0) or 0
        local threatId = DiseaseGrid.mapId ~= nil and (getBitVectorMapPoint(DiseaseGrid.mapId, x, z, 2, 4) or 0) or 0
        DiseaseGrid:recordChangedCell(x, z, severity, threatId, value)
    end
    return true
end

function DiseaseProtectionGrid:protectParallelogram(startNode, widthNode, heightNode, extraMargin, category)
    if self.mapId == nil or startNode == nil or widthNode == nil or heightNode == nil or categoryBit(category) == 0 then return 0 end
    local sx, _, sz = getWorldTranslation(startNode)
    local wx, _, wz = getWorldTranslation(widthNode)
    local hx, _, hz = getWorldTranslation(heightNode)
    local margin = math.max(0, tonumber(extraMargin) or 0)
    local function normalize(x, z)
        local length = math.sqrt(x * x + z * z)
        return length > 0.0001 and x / length or 0, length > 0.0001 and z / length or 0
    end
    if margin > 0 then
        local wdx, wdz = normalize(wx - sx, wz - sz)
        local hdx, hdz = normalize(hx - sx, hz - sz)
        sx, sz = sx - (wdx + hdx) * margin, sz - (wdz + hdz) * margin
        wx, wz = wx + wdx * margin - hdx * margin, wz + wdz * margin - hdz * margin
        hx, hz = hx - wdx * margin + hdx * margin, hz - wdz * margin + hdz * margin
    end
    local startX, startZ = DiseaseGrid:worldToLocal(sx, sz)
    local widthX, widthZ = DiseaseGrid:worldToLocal(wx, wz)
    local heightX, heightZ = DiseaseGrid:worldToLocal(hx, hz)
    local ax, az, bx, bz = wx - sx, wz - sz, hx - sx, hz - sz
    local det = ax * bz - az * bx
    if math.abs(det) < 0.0001 then return 0 end
    local changed = 0
    for z = math.max(0, math.min(startZ, widthZ, heightZ)), math.min(self.height - 1, math.max(startZ, widthZ, heightZ)) do
        for x = math.max(0, math.min(startX, widthX, heightX)), math.min(self.width - 1, math.max(startX, widthX, heightX)) do
            local px, pz = mapToWorld(self.width, self.height, x, z)
            local rx, rz = px - sx, pz - sz
            local u, v = (rx * bz - rz * bx) / det, (ax * rz - az * rx) / det
            if u >= 0 and u <= 1 and v >= 0 and v <= 1 then
                local isFieldGround = DiseaseGrid.getIsFieldGroundAtMapCell == nil
                    or DiseaseGrid:getIsFieldGroundAtMapCell(x, z)
                if isFieldGround then
                    if self:addProtectionCell(x, z, category) then changed = changed + 1 end
                elseif self:getValue(x, z) > 0 then
                    -- Remove legacy protection that was painted beyond the
                    -- actual field boundary by an earlier mod version.
                    if self:setCellValue(x, z, 0, false) then changed = changed + 1 end
                end
            end
        end
    end
    return changed
end

function DiseaseProtectionGrid:clearField(field)
    local polygon = getFieldPolygon2D(field)
    if self.mapId == nil or polygon == nil then return 0 end
    local minWX, minWZ, maxWX, maxWZ = getFieldWorldBounds(field)
    if minWX == nil then return 0 end
    local startX, startZ = DiseaseGrid:worldToLocal(minWX, minWZ)
    local endX, endZ = DiseaseGrid:worldToLocal(maxWX, maxWZ)
    local changed = 0
    for z = math.max(0, startZ), math.min(self.height - 1, endZ) do
        for x = math.max(0, startX), math.min(self.width - 1, endX) do
            if self:getValue(x, z) > 0 then
                local wx, wz = mapToWorld(self.width, self.height, x, z)
                if polygon:getIsPosInside(wx, wz) and self:setCellValue(x, z, 0, false) then changed = changed + 1 end
            end
        end
    end
    return changed
end

function DiseaseProtectionGrid:applyNetworkCell(x, z, value)
    self:setCellValue(x, z, value or 0, true)
end

addModEventListener(DiseaseProtectionGrid)
