FieldWetnessMapFrame = {}
FieldWetnessMapFrame.MOD_NAME = g_currentModName or "FS25_MudSystemPhysics"
FieldWetnessMapFrame.OVERLAY_RESOLUTION = 1024
FieldWetnessMapFrame.UPDATE_INTERVAL_MS = 1000

local FieldWetnessMapFrame_mt = Class(FieldWetnessMapFrame)

FieldWetnessMapFrame.RANGES = {
    {
        minValue = 0.00,
        maxValue = 0.20,
        labelKey = "mudsys_mapWetnessDry",
        fallback = "Dry (0-20%)",
        color = {0.88, 0.48, 0.12, 1},
        colorBlind = {0.92, 0.48, 0.08, 1}
    },
    {
        minValue = 0.20,
        maxValue = 0.40,
        labelKey = "mudsys_mapWetnessSlight",
        fallback = "Slightly moist (20-40%)",
        color = {0.88, 0.78, 0.18, 1},
        colorBlind = {0.93, 0.78, 0.16, 1}
    },
    {
        minValue = 0.40,
        maxValue = 0.60,
        labelKey = "mudsys_mapWetnessMedium",
        fallback = "Moist (40-60%)",
        color = {0.22, 0.72, 0.34, 1},
        colorBlind = {0.48, 0.74, 0.34, 1}
    },
    {
        minValue = 0.60,
        maxValue = 0.80,
        labelKey = "mudsys_mapWetnessWet",
        fallback = "Wet (60-80%)",
        color = {0.08, 0.58, 0.88, 1},
        colorBlind = {0.16, 0.62, 0.92, 1}
    },
    {
        minValue = 0.80,
        maxValue = 1.00,
        labelKey = "mudsys_mapWetnessSaturated",
        fallback = "Waterlogged (80-100%)",
        color = {0.12, 0.20, 0.90, 1},
        colorBlind = {0.52, 0.22, 0.86, 1}
    }
}

local function fwmLerp(a, b, t)
    t = math.max(0, math.min(1, tonumber(t) or 0))
    return a + (b - a) * t
end

local function fwmGetText(key, fallback)
    if g_i18n ~= nil and g_i18n.hasText ~= nil and g_i18n:hasText(key) then
        return g_i18n:getText(key)
    end
    return fallback or key
end

local function fwmResetOverlay(overlayId)
    if overlayId == nil or overlayId == 0 then
        return
    end
    resetDensityMapVisualizationOverlay(overlayId)
    setOverlayColor(overlayId, 1, 1, 1, 1)
end

local function fwmGetRangeIndex(normalized)
    return math.min(
        #FieldWetnessMapFrame.RANGES,
        math.floor(math.max(0, math.min(0.999999, normalized)) * #FieldWetnessMapFrame.RANGES) + 1
    )
end

local function fwmGetGradientColor(normalized, colorBlind)
    local ranges = FieldWetnessMapFrame.RANGES
    local scaled = math.max(0, math.min(1, normalized)) * (#ranges - 1)
    local first = math.floor(scaled) + 1
    local second = math.min(#ranges, first + 1)
    local t = scaled - math.floor(scaled)
    local colorA = colorBlind and ranges[first].colorBlind or ranges[first].color
    local colorB = colorBlind and ranges[second].colorBlind or ranges[second].color
    return fwmLerp(colorA[1], colorB[1], t),
        fwmLerp(colorA[2], colorB[2], t),
        fwmLerp(colorA[3], colorB[3], t)
end

function FieldWetnessMapFrame.new(customMt)
    local self = setmetatable({}, customMt or FieldWetnessMapFrame_mt)
    self.inGameMenuMapFrame = nil
    self.overlay = nil
    self.overlayReady = false
    self.overlayTimer = 0
    self.visibilityTimer = 0
    self.filter = {}
    for index = 1, #self.RANGES do
        self.filter[index] = true
    end
    self.isColorBlindMode = false
    self.isInstalled = false
    return self
end

function FieldWetnessMapFrame:getIsEnabled()
    return FieldLocalWetness ~= nil and FieldLocalWetness.enabled == true
end

function FieldWetnessMapFrame:getIsLayerAvailable()
    return self:getIsEnabled()
        and FieldLocalWetnessLayer ~= nil
        and FieldLocalWetnessLayer.map ~= nil
        and FieldLocalWetnessLayer.map ~= 0
end

function FieldWetnessMapFrame:getIsPageActive()
    local frame = self.inGameMenuMapFrame
    return self:getIsEnabled()
        and frame ~= nil
        and frame.mapOverviewSelector ~= nil
        and frame.mudSystemWetnessPageIndex ~= nil
        and frame.mapOverviewSelector:getState()
            == frame.mudSystemWetnessPageIndex
end

function FieldWetnessMapFrame:getDisplayValues()
    local values = {}
    for _, range in ipairs(self.RANGES) do
        table.insert(values, {
            colors = {
                [false] = {range.color},
                [true] = {range.colorBlind}
            },
            description = fwmGetText(range.labelKey, range.fallback)
        })
    end
    return values
end

function FieldWetnessMapFrame:createOverlay()
    if self.overlay == nil or self.overlay == 0 then
        self.overlay = createDensityMapVisualizationOverlay(
            "mudSystemLocalWetnessPda",
            self.OVERLAY_RESOLUTION,
            self.OVERLAY_RESOLUTION
        )
        self.overlayReady = false
    end
end

function FieldWetnessMapFrame:deleteOverlay()
    if self.overlay ~= nil and self.overlay ~= 0 then
        delete(self.overlay)
    end
    self.overlay = nil
    self.overlayReady = false
end

function FieldWetnessMapFrame:buildOverlay()
    self:createOverlay()
    fwmResetOverlay(self.overlay)

    if not self:getIsLayerAvailable() then
        generateDensityMapVisualizationOverlay(self.overlay)
        self.overlayReady = false
        return
    end

    local mapId = FieldLocalWetnessLayer.map
    local colorBlind = self.isColorBlindMode == true
    for rawValue = 0, FieldLocalWetnessLayer.MAX_VALUE do
        local normalized = rawValue / FieldLocalWetnessLayer.MAX_VALUE
        local rangeIndex = fwmGetRangeIndex(normalized)
        if self.filter[rangeIndex] == true then
            local r, g, b = fwmGetGradientColor(normalized, colorBlind)
            setDensityMapVisualizationOverlayStateColor(
                self.overlay,
                mapId,
                0,
                0,
                0,
                FieldLocalWetnessLayer.NUM_CHANNELS,
                rawValue,
                r,
                g,
                b
            )
        end
    end

    generateDensityMapVisualizationOverlay(self.overlay)
    self.overlayReady = false
end

function FieldWetnessMapFrame:updatePageData(frame)
    frame = frame or self.inGameMenuMapFrame
    local pageIndex = frame ~= nil and frame.mudSystemWetnessPageIndex or nil
    if pageIndex == nil then
        return
    end

    frame.dataTables[pageIndex] = self:getDisplayValues()
    frame.filterStates[pageIndex] = self.filter
    local selected = 0
    for index = 1, #self.RANGES do
        if self.filter[index] then
            selected = selected + 1
        end
    end
    frame.numSelectedFilters[pageIndex] = selected
    if frame.buttonDeselectAllText ~= nil then
        local symbol = selected == 0
            and InGameMenuMapFrame.L10N_SYMBOL.SELECT_ALL
            or InGameMenuMapFrame.L10N_SYMBOL.DESELECT_ALL
        frame.buttonDeselectAllText:setText(g_i18n:getText(symbol))
    end
end

function FieldWetnessMapFrame:refreshCategoryDots(frame)
    if frame == nil or frame.subCategoryDotBox == nil then
        return
    end
    for index, dot in pairs(frame.subCategoryDotBox.elements) do
        local currentIndex = index
        function dot.getIsSelected()
            return frame.mapOverviewSelector:getState() == currentIndex
        end
    end
    frame.subCategoryDotBox:invalidateLayout()
end

function FieldWetnessMapFrame:addPage(frame)
    if frame == nil or frame.mudSystemWetnessPageIndex ~= nil
        or frame.mapSelectorTexts == nil or frame.mapOverviewSelector == nil then
        return false
    end

    table.insert(
        frame.mapSelectorTexts,
        fwmGetText("mudsys_mapOverviewWetness", "Soil wetness")
    )
    frame.mudSystemWetnessPageIndex = #frame.mapSelectorTexts
    frame.mapOverviewSelector:setTexts(frame.mapSelectorTexts)
    self:updatePageData(frame)

    local dotBox = frame.subCategoryDotBox
    if dotBox ~= nil and dotBox.elements ~= nil and dotBox.elements[1] ~= nil then
        dotBox:addElement(dotBox.elements[1]:clone(dotBox))
        self:refreshCategoryDots(frame)
    end
    if frame.filterList ~= nil then
        frame.filterList:reloadData()
    end
    return true
end

function FieldWetnessMapFrame:removePage(frame)
    local pageIndex = frame ~= nil and frame.mudSystemWetnessPageIndex or nil
    if pageIndex == nil then
        return true
    end

    -- The page is appended after every normal setupMapOverview extension has
    -- finished, so it is the tail item and can be removed without shifting the
    -- page indices owned by Precision Farming or other mods.
    if pageIndex ~= #frame.mapSelectorTexts then
        return false
    end

    if frame.mapOverviewSelector:getState() == pageIndex then
        frame.mapOverviewSelector:setState(1, true)
        frame:onClickMapOverviewSelector(1)
    end

    table.remove(frame.mapSelectorTexts, pageIndex)
    frame.dataTables[pageIndex] = nil
    frame.filterStates[pageIndex] = nil
    frame.numSelectedFilters[pageIndex] = nil
    frame.mudSystemWetnessPageIndex = nil
    frame.mapOverviewSelector:setTexts(frame.mapSelectorTexts)

    local dotBox = frame.subCategoryDotBox
    if dotBox ~= nil and dotBox.elements ~= nil
        and #dotBox.elements > #frame.mapSelectorTexts then
        dotBox.elements[#dotBox.elements]:delete()
        self:refreshCategoryDots(frame)
    end
    if frame.filterList ~= nil then
        frame.filterList:reloadData()
    end
    return true
end

function FieldWetnessMapFrame:syncPageVisibility(frame)
    frame = frame or self.inGameMenuMapFrame
    if frame == nil or frame.isMapOverviewInitialized ~= true then
        return
    end
    if self:getIsEnabled() then
        self:addPage(frame)
    else
        self:removePage(frame)
    end
end

function FieldWetnessMapFrame:drawOverlay(x, y, width, height)
    if self.overlay == nil or self.overlay == 0 then
        return
    end
    if not self.overlayReady then
        local densityReady = getIsDensityMapVisualizationOverlayReady == nil
            or getIsDensityMapVisualizationOverlayReady(self.overlay)
        local overlayReady = getIsOverlayReady == nil or getIsOverlayReady(self.overlay)
        self.overlayReady = densityReady and overlayReady
    end
    if self.overlayReady then
        setOverlayUVs(self.overlay, 0, 0, 0, 1, 1, 0, 1, 1)
        renderOverlay(self.overlay, x, y, width, height)
    end
end

function FieldWetnessMapFrame:loadMap()
    self.overlayTimer = 0
    self.visibilityTimer = 0
    self.inGameMenuMapFrame = nil
    if g_gameSettings ~= nil and GameSettings ~= nil
        and GameSettings.SETTING ~= nil
        and GameSettings.SETTING.USE_COLORBLIND_MODE ~= nil then
        self.isColorBlindMode = g_gameSettings:getValue(
            GameSettings.SETTING.USE_COLORBLIND_MODE
        ) == true
    end
    for index = 1, #self.RANGES do
        self.filter[index] = true
    end
end

function FieldWetnessMapFrame:deleteMap()
    if self.inGameMenuMapFrame ~= nil then
        self:removePage(self.inGameMenuMapFrame)
    end
    self:deleteOverlay()
    self.inGameMenuMapFrame = nil
end

function FieldWetnessMapFrame:install()
    if self.isInstalled or InGameMenuMapFrame == nil
        or Utils == nil or Utils.overwrittenFunction == nil then
        return
    end

    InGameMenuMapFrame.onLoadMapFinished = Utils.overwrittenFunction(
        InGameMenuMapFrame.onLoadMapFinished,
        function(frame, superFunc, ...)
            superFunc(frame, ...)
            self.inGameMenuMapFrame = frame
            self:createOverlay()

            function frame.mapOverviewSelector.onClickCallback(_, state)
                frame:onClickMapOverviewSelector(state)
            end
            frame.ingameMap.onDrawPostIngameMapCallback = InGameMenuMapFrame.onDrawPostIngameMap
            frame.ingameMap.onDrawPostIngameMapHotspotsCallback = InGameMenuMapFrame.onDrawPostIngameMapHotspots
            frame.ingameMap.onClickMapCallback = InGameMenuMapFrame.onClickMap
        end
    )

    InGameMenuMapFrame.setupMapOverview = Utils.overwrittenFunction(
        InGameMenuMapFrame.setupMapOverview,
        function(frame, superFunc, ...)
            superFunc(frame, ...)
            self.inGameMenuMapFrame = frame
            frame.mudSystemWetnessPageIndex = nil
            self.visibilityTimer = 0
        end
    )

    InGameMenuMapFrame.onClickMapOverviewSelector = Utils.overwrittenFunction(
        InGameMenuMapFrame.onClickMapOverviewSelector,
        function(frame, superFunc, state)
            superFunc(frame, state)
            if self:getIsEnabled() and state == frame.mudSystemWetnessPageIndex then
                self:updatePageData(frame)
                frame.filterListContainer:setVisible(true)
                frame.buttonDeselectAllContainer:setVisible(true)
                frame.filterList:reloadData()
                self:buildOverlay()
            end
        end
    )

    InGameMenuMapFrame.getHasChangeableFilterList = Utils.overwrittenFunction(
        InGameMenuMapFrame.getHasChangeableFilterList,
        function(frame, superFunc, ...)
            return superFunc(frame, ...)
                or (self:getIsEnabled()
                    and frame.mapOverviewSelector:getState() == frame.mudSystemWetnessPageIndex)
        end
    )

    InGameMenuMapFrame.onClickDeselectAll = Utils.overwrittenFunction(
        InGameMenuMapFrame.onClickDeselectAll,
        function(frame, superFunc, ...)
            local isWetnessPage = self:getIsEnabled()
                and frame.mapOverviewSelector:getState() == frame.mudSystemWetnessPageIndex
            superFunc(frame, ...)
            if isWetnessPage then
                self:updatePageData(frame)
                self:buildOverlay()
            end
        end
    )

    InGameMenuMapFrame.onDrawPostIngameMap = Utils.overwrittenFunction(
        InGameMenuMapFrame.onDrawPostIngameMap,
        function(frame, superFunc, element, ingameMap, ...)
            if self:getIsEnabled()
                and frame.mapOverviewSelector:getState() == frame.mudSystemWetnessPageIndex then
                local previousHideOverlay = frame.hideContentOverlay
                frame.hideContentOverlay = true
                superFunc(frame, element, ingameMap, ...)
                frame.hideContentOverlay = previousHideOverlay

                local width, height = frame.ingameMapBase.fullScreenLayout:getMapSize()
                local x, y = frame.ingameMapBase.fullScreenLayout:getMapPosition()
                self:drawOverlay(
                    x + width * 0.25,
                    y + height * 0.25,
                    width * 0.5,
                    height * 0.5
                )
                if frame.dynamicMapImageLoadingBg ~= nil and self.overlayReady then
                    frame.dynamicMapImageLoadingBg:setVisible(false)
                end
                return
            end
            superFunc(frame, element, ingameMap, ...)
        end
    )

    InGameMenuMapFrame.generateOverviewOverlay = Utils.overwrittenFunction(
        InGameMenuMapFrame.generateOverviewOverlay,
        function(frame, superFunc, ...)
            superFunc(frame, ...)
            if self:getIsEnabled()
                and frame.mapOverviewSelector:getState() == frame.mudSystemWetnessPageIndex then
                self:buildOverlay()
            end
        end
    )

    InGameMenuMapFrame.update = Utils.overwrittenFunction(
        InGameMenuMapFrame.update,
        function(frame, superFunc, dt)
            superFunc(frame, dt)
            self.inGameMenuMapFrame = frame

            self.visibilityTimer = (self.visibilityTimer or 0) + (dt or 0)
            if self.visibilityTimer >= 250 then
                self.visibilityTimer = 0
                self:syncPageVisibility(frame)
            end

            if not self:getIsEnabled()
                or frame.mapOverviewSelector:getState() ~= frame.mudSystemWetnessPageIndex then
                self.overlayTimer = 0
                return
            end

            self.overlayTimer = (self.overlayTimer or 0) + (dt or 0)
            if self.overlayTimer >= self.UPDATE_INTERVAL_MS then
                self.overlayTimer = 0
                self:buildOverlay()
            end
        end
    )

    if InGameMenuMapFrame.setColorBlindMode ~= nil then
        InGameMenuMapFrame.setColorBlindMode = Utils.overwrittenFunction(
            InGameMenuMapFrame.setColorBlindMode,
            function(frame, superFunc, isColorBlindMode)
                superFunc(frame, isColorBlindMode)
                self.isColorBlindMode = isColorBlindMode == true
                if self:getIsEnabled()
                    and frame.mapOverviewSelector:getState() == frame.mudSystemWetnessPageIndex then
                    self:buildOverlay()
                end
            end
        )
    end

    self.isInstalled = true
end

FieldWetnessMapFrame = FieldWetnessMapFrame.new()
FieldWetnessMapFrame:install()
addModEventListener(FieldWetnessMapFrame)
