local UI = nil
local virtualFloor = 7
local viewRadioGroup = nil
local loadedAssetsDir = nil
local loadedFloorSet = {} -- floors already indexed for loadedAssetsDir

-- Forward declaration — defined later in the file.
local refreshVirtualFloors

-- Surface View is only available for floors 0-7 (surface and above).
-- Underground floors (8-15) always use Map View.
local SEA_FLOOR = 7

local function ensureViewFloorsLoaded()
    local assetsDir = string.format("/data/things/%d", g_game.getClientVersion())

    -- Reset when switching game versions
    if assetsDir ~= loadedAssetsDir then
        g_satelliteMap.clear()
        loadedFloorSet = {}
        loadedAssetsDir = assetsDir
    end

    -- For surface floors: composite view needs [virtualFloor, SEA_FLOOR].
    -- For underground: only the current floor is needed (static minimap, map view only).
    local floorMax = (virtualFloor <= SEA_FLOOR) and SEA_FLOOR or virtualFloor

    local minNeeded, maxNeeded
    for f = virtualFloor, floorMax do
        if not loadedFloorSet[f] then
            if not minNeeded then
                minNeeded = f
            end
            maxNeeded = f
        end
    end

    if minNeeded then
        g_satelliteMap.loadFloors(assetsDir, minNeeded, maxNeeded)
        for f = minNeeded, maxNeeded do
            loadedFloorSet[f] = true
        end
    end
end

local function isSurfaceFloor(floor)
    return floor <= SEA_FLOOR
end

local function updateViewMode()
    if not UI or not viewRadioGroup then
        return
    end

    local minimapWidget = UI.MapBase.minimap
    local viewBase = UI.InformationBase.InternalBase.DisplayBase.ViewBase1
    local surfaceCheck = viewBase.SurfaceCheck
    local separatorScroll = viewBase.SeparatorScroll
    local separatorLabel = viewBase.SeparatorLabel

    local canUseSurface = isSurfaceFloor(virtualFloor) and g_satelliteMap.hasChunksForView(virtualFloor)

    -- Underground: force Map View and lock the Surface option.
    -- selectWidget must come BEFORE setEnabled: UIRadioGroup calls setChecked(false)
    -- on the deselected widget, which does not work correctly on a disabled widget.
    if not canUseSurface then
        viewRadioGroup:selectWidget(viewBase.MapCheck)
    end
    surfaceCheck:setEnabled(canUseSurface)

    -- Level separator scroll is only meaningful in Surface View on floors above z7 (z < 7).
    -- At z7 itself the composite has no lower floor to blend with, so the scroll is unused.
    local inSurfaceMode = canUseSurface and surfaceCheck:isChecked() and virtualFloor < SEA_FLOOR
    separatorScroll:setEnabled(inSurfaceMode)
    separatorLabel:setEnabled(inSurfaceMode)
end

function showMap()
    g_minimap.saveOtmm('/minimap.otmm')
    UI = g_ui.loadUI("map", contentContainer)
    UI:show()
    controllerCyclopedia:registerEvents(LocalPlayer, {
        onPositionChange = Cyclopedia.onUpdateCameraPosition
    }):execute()

    Cyclopedia.loadMap()

    controllerCyclopedia.ui.CharmsBase:setVisible(false)
    controllerCyclopedia.ui.GoldBase:setVisible(true)
    controllerCyclopedia.ui.BestiaryTrackerButton:setVisible(false)
    if g_game.getClientVersion() >= 1410 then
        controllerCyclopedia.ui.CharmsBase1410:setVisible(false)
    end
end

function Cyclopedia.loadMap()
    local clientVersion = g_game.getClientVersion()
    local minimapWidget = UI.MapBase.minimap

    -- Prime virtualFloor from current player position before any floor-dependent logic
    local player = g_game.getLocalPlayer()
    if player then
        local pos = player:getPosition()
        if pos then
            virtualFloor = pos.z
        end
    end

    g_minimap.clean()

    local loaded = false
    local minimapFile = "/minimap.otmm"
    local dataMinimapFile = "/data" .. minimapFile
    local versionedMinimapFile = "/minimap" .. clientVersion .. ".otmm"

    if g_resources.fileExists(dataMinimapFile) then
        loaded = g_minimap.loadOtmm(dataMinimapFile)
    end

    if not loaded and g_resources.fileExists(versionedMinimapFile) then
        loaded = g_minimap.loadOtmm(versionedMinimapFile)
    end

    if not loaded and g_resources.fileExists(minimapFile) then
        loaded = g_minimap.loadOtmm(minimapFile)
    end

    if not loaded then
        print("Minimap couldn't be loaded, file missing?")
    end

    -- Preserve the zoom declared in the OTUI style — load() overwrites it with the
    -- HUD minimap's saved settings zoom, which we don't want for Cyclopedia.
    local initialZoom = minimapWidget:getZoom()
    minimapWidget:load()
    minimapWidget:setZoom(initialZoom)
    minimapWidget:setUseStaticMinimap(true)

    -- Load satellite chunks for the floors needed by the current view only.
    -- Additional floors are indexed on demand when the user navigates deeper.
    ensureViewFloorsLoaded()
    local chunkCount = g_satelliteMap.hasChunksForView(virtualFloor) and 1 or 0

    -- Views panel is always visible; satellite chunks determine whether Surface View is enabled.
    local viewBase = UI.InformationBase.InternalBase.DisplayBase.ViewBase1
    local surfaceCheck = viewBase.SurfaceCheck
    local mapCheck = viewBase.MapCheck

    -- Clean up previous radio group if the map tab was reopened
    if viewRadioGroup then
        viewRadioGroup:destroy()
        viewRadioGroup = nil
    end

    viewRadioGroup = UIRadioGroup.create()
    viewRadioGroup:addWidget(surfaceCheck)
    viewRadioGroup:addWidget(mapCheck)

    viewRadioGroup.onSelectionChange = function(self, selected)
        local inSurface = selected == surfaceCheck and isSurfaceFloor(virtualFloor) and
                              g_satelliteMap.hasChunksForView(virtualFloor)
        minimapWidget:setSatelliteMode(inSurface)

        -- Sync separator scroll enabled state (only for floors above z7)
        local separatorScroll = viewBase.SeparatorScroll
        local separatorLabel = viewBase.SeparatorLabel
        local canUseSeparator = inSurface and virtualFloor < SEA_FLOOR
        separatorScroll:setEnabled(canUseSeparator)
        separatorLabel:setEnabled(canUseSeparator)
    end

    -- Default: Surface View when satellite data exists and on a surface floor, else Map View
    viewRadioGroup:selectWidget((chunkCount > 0 and isSurfaceFloor(virtualFloor)) and surfaceCheck or mapCheck)

    -- Hook separator scroll: adjusts floor compositing opacity (1.0 = all visible, 0.0 = target only)
    local separatorScroll = viewBase.SeparatorScroll
    separatorScroll.onValueChange = function(self, value)
        minimapWidget:setFloorSeparatorOpacity(value / 100.0)
    end
    -- Reset opacity on (re-)open
    minimapWidget:setFloorSeparatorOpacity(separatorScroll:getValue() / 100.0)

    updateViewMode()

    -- Sync flags from main minimap's live table (avoids stale g_settings on first open).
    local mainMinimap = modules.game_minimap and modules.game_minimap.getMiniMapUi and
                            modules.game_minimap.getMiniMapUi()
    if mainMinimap then
        for _, flag in pairs(mainMinimap.flags) do
            if not minimapWidget:getFlag(flag.pos) then
                minimapWidget:addFlag(flag.pos, flag.icon, flag.description)
            end
        end
    end

    -- Apply initial filter state: hide flags whose filter checkbox is unchecked.
    local markList = UI.InformationBase.InternalBase.DisplayBase.MarkList
    for _, flag in pairs(minimapWidget.flags) do
        local btn = markList:getChildById(tostring(flag.icon))
        flag:setVisible(btn ~= nil and btn:isChecked())
    end

    refreshVirtualFloors()
end

function Cyclopedia.toggleMapFlag(widget, checked)
    local flagType = tonumber(widget:getId())
    if not flagType then
        return
    end
    local minimapWidget = UI.MapBase.minimap
    for _, flag in pairs(minimapWidget.flags) do
        if flag.icon == flagType then
            flag:setVisible(checked)
        end
    end
end

function Cyclopedia.showAllFlags(checked)
    local list = UI.InformationBase.InternalBase.DisplayBase.MarkList
    for i = 0, list:getChildCount() - 1 do
        local btn = list:getChildByIndex(i)
        if btn then
            btn:setChecked(checked)
        end
    end
    local minimapWidget = UI.MapBase.minimap
    for _, flag in pairs(minimapWidget.flags) do
        flag:setVisible(checked)
    end
end

function Cyclopedia.moveMap(widget)
    local distance = 5
    local direction = widget:getId()
    if direction == "n" then
        UI.MapBase.minimap:move(0, distance)
    elseif direction == "ne" then
        UI.MapBase.minimap:move(-distance, distance)
    elseif direction == "e" then
        UI.MapBase.minimap:move(-distance, 0)
    elseif direction == "se" then
        UI.MapBase.minimap:move(-distance, -distance)
    elseif direction == "s" then
        UI.MapBase.minimap:move(0, -distance)
    elseif direction == "sw" then
        UI.MapBase.minimap:move(distance, -distance)
    elseif direction == "w" then
        UI.MapBase.minimap:move(distance, 0)
    elseif direction == "nw" then
        UI.MapBase.minimap:move(distance, distance)
    end
end

function Cyclopedia.floorScrollBar(oldValue, value)
    if value < oldValue then
        UI.MapBase.minimap:floorUp()
    elseif oldValue < value then
        UI.MapBase.minimap:floorDown()
    end

    if value < 0 then
        value = 0
    elseif value > 15 then
        value = 15
    end
end

function ConvertLayer(Value)
    if Value == 150 then
        return 7
    elseif Value == 300 then
        return 15
    elseif Value >= 1 and Value <= 300 then
        return math.floor((Value - 1) / 20)
    else
        return 0
    end
end

refreshVirtualFloors = function()
    local mark = UI.InformationBase.InternalBase.NavigationBase.layersMark
    mark:setMarginTop(((virtualFloor + 1) * 4) - 3)
    UI.InformationBase.InternalBase.NavigationBase.automapLayers:setImageClip((virtualFloor * 14) .. ' 0 14 67')

    -- Update player marker (cross) visibility on the minimap
    local cross = UI.MapBase.minimap.cross
    if cross then
        if virtualFloor > SEA_FLOOR then
            cross:setVisible(false)
        elseif virtualFloor < SEA_FLOOR then
            cross:setVisible(true)
            cross:setIconColor("#ffffff80")
        else
            cross:setVisible(true)
            cross:setIconColor("#ffffffff")
        end
    end
end

function Cyclopedia.onUpdateCameraPosition()
    local player = g_game.getLocalPlayer()
    if not player then
        return
    end

    local pos = player:getPosition()
    if not pos then
        return
    end

    local minimapWidget = UI.MapBase.minimap
    if not minimapWidget:isDragging() then
        if not fullmapView then
            minimapWidget:setCameraPosition(player:getPosition())
        end

        minimapWidget:setCrossPosition(player:getPosition(), true)
    end

    local prevFloor = virtualFloor
    virtualFloor = pos.z

    -- When the player changes floor, sync the visual floor indicator and
    -- satellite/map view toggle (e.g. walking underground disables Surface View).
    if virtualFloor ~= prevFloor then
        ensureViewFloorsLoaded()
        refreshVirtualFloors()
        updateViewMode()
    end
end

function Cyclopedia.onClickRoseButton(dir)
    if dir == 'north' then
        UI.MapBase.minimap:move(0, 1)
    elseif dir == 'north-east' then
        UI.MapBase.minimap:move(-1, 1)
    elseif dir == 'east' then
        UI.MapBase.minimap:move(-1, 0)
    elseif dir == 'south-east' then
        UI.MapBase.minimap:move(-1, -1)
    elseif dir == 'south' then
        UI.MapBase.minimap:move(0, -1)
    elseif dir == 'south-west' then
        UI.MapBase.minimap:move(1, -1)
    elseif dir == 'west' then
        UI.MapBase.minimap:move(1, 0)
    elseif dir == 'north-west' then
        UI.MapBase.minimap:move(1, 1)
    end
end

function Cyclopedia.setZoom(zoom)
    if zoom then
        UI.MapBase.minimap:zoomIn()
    else
        UI.MapBase.minimap:zoomOut()
    end
end

function Cyclopedia.downLayer()
    if virtualFloor == 15 then
        return
    end

    UI.MapBase.minimap:floorDown(1)
    virtualFloor = virtualFloor + 1
    ensureViewFloorsLoaded()
    refreshVirtualFloors()
    updateViewMode()
end

function Cyclopedia.upLayer()
    if virtualFloor == 0 then
        return
    end

    UI.MapBase.minimap:floorUp(1)
    virtualFloor = virtualFloor - 1
    ensureViewFloorsLoaded()
    refreshVirtualFloors()
    updateViewMode()
end
