local _, Addon = ...

local MapView = {}
Addon.MapView = MapView
local L = Addon.L

local state = Addon.state
local Coordinates = Addon.Coordinates
local mapFrame
local viewport
local canvas
local titleText
local statusText
local helpText
local shapeMask
local tiles = {}
local explorationTiles = {}

local function IsInstancedContent()
    local inInstance, instanceType = IsInInstance()
    return inInstance and instanceType and instanceType ~= "none"
end

local function SetProtectedOverviewState()
    Addon.db.zoom = 1
    state.focusX = 0.5
    state.focusY = 0.5
    state.panX = 0
    state.panY = 0
    state.followPlayer = true
    state.autoCenterUntil = 0
    state.protectedCenterMode = "vista completa automática"
    state.protectedCenterError = nil
end

local function GetSavedProtectedFocus(mapID)
    local saved = Addon.db.protectedFocusByMap and Addon.db.protectedFocusByMap[mapID]
    if type(saved) == "table" and Addon:IsValidMapCoordinate(saved.x, saved.y) then
        return saved.x, saved.y
    end
    return nil
end

local function ApplyShapeMask(texture)
    if texture._mzmShapeMask == shapeMask then
        return
    end
    if texture._mzmShapeMask then
        pcall(texture.RemoveMaskTexture, texture, texture._mzmShapeMask)
    end
    texture._mzmShapeMask = nil
    if shapeMask then
        local ok = pcall(texture.AddMaskTexture, texture, shapeMask)
        if ok then
            texture._mzmShapeMask = shapeMask
        end
    end
end

local function AcquireTile(index)
    if not tiles[index] then
        local texture = canvas:CreateTexture(nil, "ARTWORK")
        texture:SetHorizTile(false)
        texture:SetVertTile(false)
        ApplyShapeMask(texture)
        tiles[index] = {
            texture = texture,
            row = 0,
            column = 0,
        }
    end

    return tiles[index]
end

local function HideUnusedTiles(firstUnused)
    for index = firstUnused, #tiles do
        tiles[index].texture:Hide()
    end
end

local function AcquireExplorationTile(index)
    if not explorationTiles[index] then
        local texture = canvas:CreateTexture(nil, "ARTWORK", nil, 1)
        ApplyShapeMask(texture)
        explorationTiles[index] = {
            texture = texture,
            pixelWidth = 1,
            pixelHeight = 1,
            offsetX = 0,
            offsetY = 0,
        }
    end

    return explorationTiles[index]
end

local function HideUnusedExplorationTiles(firstUnused)
    for index = firstUnused, #explorationTiles do
        explorationTiles[index].texture:Hide()
    end
end

local function NextPowerOfTwo(value)
    local size = 16
    while size < value do
        size = size * 2
    end
    return size
end

function MapView:BuildExplorationOverlays()
    if not state.mapID or not state.layer then
        HideUnusedExplorationTiles(1)
        return
    end

    local exploredTextures = C_MapExplorationInfo.GetExploredMapTextures(state.mapID)
    if not exploredTextures then
        HideUnusedExplorationTiles(1)
        return
    end

    local tileWidth = state.layer.tileWidth
    local tileHeight = state.layer.tileHeight
    local explorationIndex = 1

    for _, overlayInfo in ipairs(exploredTextures) do
        -- Blizzard reserva las texturas de mouseover para previsualizar zonas
        -- no exploradas; no deben quedar dibujadas de forma permanente.
        if not overlayInfo.isShownByMouseOver then
            local columns = math.ceil(overlayInfo.textureWidth / tileWidth)
            local rows = math.ceil(overlayInfo.textureHeight / tileHeight)

            for row = 1, rows do
                local pixelHeight = tileHeight
                local fileHeight = tileHeight
                if row == rows then
                    pixelHeight = overlayInfo.textureHeight % tileHeight
                    if pixelHeight == 0 then
                        pixelHeight = tileHeight
                    end
                    fileHeight = NextPowerOfTwo(pixelHeight)
                end

                for column = 1, columns do
                    local pixelWidth = tileWidth
                    local fileWidth = tileWidth
                    if column == columns then
                        pixelWidth = overlayInfo.textureWidth % tileWidth
                        if pixelWidth == 0 then
                            pixelWidth = tileWidth
                        end
                        fileWidth = NextPowerOfTwo(pixelWidth)
                    end

                    local fileIndex = ((row - 1) * columns) + column
                    local fileDataID = overlayInfo.fileDataIDs[fileIndex]
                    if fileDataID then
                        local tile = AcquireExplorationTile(explorationIndex)
                        tile.pixelWidth = pixelWidth
                        tile.pixelHeight = pixelHeight
                        tile.offsetX = overlayInfo.offsetX + (tileWidth * (column - 1))
                        tile.offsetY = overlayInfo.offsetY + (tileHeight * (row - 1))
                        tile.texture:SetDrawLayer("ARTWORK", overlayInfo.isDrawOnTopLayer and 2 or 1)
                        tile.texture:SetTexCoord(0, pixelWidth / fileWidth, 0, pixelHeight / fileHeight)
                        tile.texture:SetTexture(fileDataID, nil, nil, "TRILINEAR")
                        tile.texture:Show()
                        explorationIndex = explorationIndex + 1
                    end
                end
            end
        end
    end

    HideUnusedExplorationTiles(explorationIndex)
end

function MapView:PlaceCanvas()
    if not state.layer then
        return
    end

    local viewWidth = viewport:GetWidth()
    local viewHeight = viewport:GetHeight()
    local canvasWidth = canvas:GetWidth()
    local canvasHeight = canvas:GetHeight()
    if viewWidth <= 1 or viewHeight <= 1 or canvasWidth <= 1 or canvasHeight <= 1 then
        return
    end

    if state.followPlayer then
        local playerX, playerY = Coordinates:GetUnitMapPosition(state.mapID, "player")
        if playerX and playerY then
            state.focusX = playerX
            state.focusY = playerY
            state.autoCenterUntil = 0
            state.protectedCenterMode = "coordenadas"
            state.protectedCenterError = nil
        elseif IsInstancedContent() and Addon.db.zoom == 1 then
            state.focusX = 0.5
            state.focusY = 0.5
            state.panX = 0
            state.panY = 0
            state.protectedCenterMode = "vista completa automática"
            state.protectedCenterError = nil
        else
            state.protectedCenterMode = GetSavedProtectedFocus(state.mapID)
                and "calibrado guardado"
                or "protegido sin calibrar"
        end
    end

    local offsetX
    if canvasWidth <= viewWidth then
        offsetX = (viewWidth - canvasWidth) / 2
    else
        offsetX = (viewWidth / 2) - (state.focusX * canvasWidth) + state.panX
        offsetX = Addon:Clamp(offsetX, viewWidth - canvasWidth, 0)
    end

    local offsetY
    if canvasHeight <= viewHeight then
        offsetY = -(viewHeight - canvasHeight) / 2
    else
        offsetY = (state.focusY * canvasHeight) - (viewHeight / 2) + state.panY
        offsetY = Addon:Clamp(offsetY, 0, canvasHeight - viewHeight)
    end

    canvas:ClearAllPoints()
    canvas:SetPoint("TOPLEFT", viewport, "TOPLEFT", offsetX, offsetY)
end

function MapView:PositionMarker(marker, x, y)
    marker:ClearAllPoints()
    marker:SetPoint("CENTER", canvas, "TOPLEFT", x * canvas:GetWidth(), -y * canvas:GetHeight())
    marker:Show()
end

function MapView:SetShapeMask(mask)
    if shapeMask == mask then
        return
    end
    shapeMask = mask
    for _, tile in ipairs(tiles) do
        ApplyShapeMask(tile.texture)
    end
    for _, tile in ipairs(explorationTiles) do
        ApplyShapeMask(tile.texture)
    end
end

function MapView:UpdateGeometry()
    if not state.layer then
        return
    end

    local viewWidth = viewport:GetWidth()
    local viewHeight = viewport:GetHeight()
    if viewWidth <= 1 or viewHeight <= 1 then
        return
    end

    local fitScale = math.min(
        viewWidth / state.layer.layerWidth,
        viewHeight / state.layer.layerHeight
    )
    state.scale = fitScale * Addon.ZOOM_LEVELS[Addon.db.zoom]

    canvas:SetSize(state.layer.layerWidth * state.scale, state.layer.layerHeight * state.scale)

    for _, tile in ipairs(tiles) do
        if tile.texture:IsShown() then
            tile.texture:ClearAllPoints()
            tile.texture:SetSize(state.layer.tileWidth * state.scale, state.layer.tileHeight * state.scale)
            tile.texture:SetPoint(
                "TOPLEFT",
                canvas,
                "TOPLEFT",
                tile.column * state.layer.tileWidth * state.scale,
                -tile.row * state.layer.tileHeight * state.scale
            )
        end
    end

    for _, tile in ipairs(explorationTiles) do
        if tile.texture:IsShown() then
            tile.texture:ClearAllPoints()
            tile.texture:SetSize(tile.pixelWidth * state.scale, tile.pixelHeight * state.scale)
            tile.texture:SetPoint(
                "TOPLEFT",
                canvas,
                "TOPLEFT",
                tile.offsetX * state.scale,
                -tile.offsetY * state.scale
            )
        end
    end

    Addon.Markers:RepositionContentMarkers()
    self:PlaceCanvas()
    Addon.Markers:UpdateMarkers()
end

function MapView:ShowMapError(message)
    state.mapID = nil
    state.layer = nil
    state.textureIDs = nil
    HideUnusedTiles(1)
    HideUnusedExplorationTiles(1)
    Addon.Markers:HideAll()
    titleText:SetText(L.APP_NAME)
    helpText:SetText(message)
    helpText:Show()
    statusText:SetText(L.NO_MAP_DATA)
end

function MapView:Rebuild(force)
    local mapID = Coordinates:ResolveMapID()
    if not mapID then
        self:ShowMapError(L.NO_USABLE_MAP)
        return
    end

    if not force and state.mapID == mapID and state.layer then
        self:UpdateGeometry()
        Addon.Markers:RefreshContentMarkers()
        return
    end

    local layers = C_Map.GetMapArtLayers(mapID)
    local layer = layers and layers[1]
    local textureIDs = layer and C_Map.GetMapArtLayerTextures(mapID, 1)
    if not layer or not textureIDs or #textureIDs == 0 then
        self:ShowMapError(L.MAP_TEXTURE_FAILED)
        return
    end

    local mapChanged = state.mapID ~= mapID
    if mapChanged then
        local previousZoom = Addon.db.zoom
        local savedZoom = tonumber(Addon.db.zoomByMap[mapID])
        Addon.db.zoom = Addon:Clamp(
            math.floor(savedZoom or previousZoom or Addon.defaults.zoom),
            1,
            #Addon.ZOOM_LEVELS
        )

        -- Nunca reutilizamos el foco normalizado del mapa anterior. Cuando
        -- Blizzard protege la posición mostramos el piso completo.
        local playerX, playerY = Coordinates:GetUnitMapPosition(mapID, "player")
        local savedX, savedY = GetSavedProtectedFocus(mapID)
        local protectedInstance = IsInstancedContent() and not playerX
        if protectedInstance then
            SetProtectedOverviewState()
        else
            state.focusX = playerX or savedX or 0.5
            state.focusY = playerY or savedY or 0.5
        end
        if IsInstancedContent() and playerX then
            Addon.db.zoom = math.max(Addon.db.zoom, Addon.defaults.zoom)
        end
        if not protectedInstance then
            state.protectedCenterMode = playerX and "coordenadas"
                or savedX and "calibrado guardado"
                or "protegido sin calibrar"
        end
        state.protectedCenterError = nil
    end

    state.mapID = mapID
    state.layer = layer
    state.textureIDs = textureIDs
    state.panX = 0
    state.panY = 0
    state.followPlayer = true

    local mapInfo = C_Map.GetMapInfo(mapID)
    titleText:SetText(mapInfo and mapInfo.name or L.APP_NAME)
    helpText:Hide()

    local columns = math.ceil(layer.layerWidth / layer.tileWidth)
    local rows = math.ceil(layer.layerHeight / layer.tileHeight)
    local tileIndex = 1
    for row = 0, rows - 1 do
        for column = 0, columns - 1 do
            local tile = AcquireTile(tileIndex)
            tile.row = row
            tile.column = column
            tile.texture:SetTexture(textureIDs[tileIndex])
            tile.texture:SetTexCoord(0, 1, 0, 1)
            tile.texture:Show()
            tileIndex = tileIndex + 1
        end
    end

    HideUnusedTiles(tileIndex)
    self:BuildExplorationOverlays()
    self:UpdateGeometry()
    Addon.Markers:RefreshContentMarkers()
    if mapChanged and IsInstancedContent() then
        Addon.Markers:HighlightPlayer()
    end
end

function MapView:ChangeZoom(direction)
    local newZoom = Addon:Clamp(Addon.db.zoom + direction, 1, #Addon.ZOOM_LEVELS)
    if newZoom == Addon.db.zoom then
        return
    end

    Addon.db.zoom = newZoom
    if state.mapID then
        Addon.db.zoomByMap[state.mapID] = Addon.db.zoom
    end
    state.panX = 0
    state.panY = 0
    state.autoCenterUntil = 0
    local playerX = state.mapID and Coordinates:GetUnitMapPosition(state.mapID, "player")
    if IsInstancedContent() and not playerX then
        if newZoom == 1 then
            SetProtectedOverviewState()
        else
            -- La posición nativa sigue visible, pero Blizzard no permite usarla
            -- para desplazar el lienzo. El zoom manual pasa a modo exploración.
            state.followPlayer = false
            state.protectedCenterMode = "exploración manual"
        end
    end
    self:UpdateGeometry()
end

function MapView:PrepareForMapChange()
    state.followPlayer = true
    state.focusX = 0.5
    state.focusY = 0.5
    state.panX = 0
    state.panY = 0
    state.autoCenterUntil = GetTime() + 5
end

function MapView:GetCursorMapPosition()
    local left = canvas:GetLeft()
    local top = canvas:GetTop()
    local width = canvas:GetWidth()
    local height = canvas:GetHeight()
    if Addon:IsSecret(left)
        or Addon:IsSecret(top)
        or not left
        or not top
        or width <= 1
        or height <= 1
    then
        return nil
    end

    local cursorX, cursorY = Addon:GetScaledCursorPosition()
    local x = (cursorX - left) / width
    local y = (top - cursorY) / height
    if not Addon:IsValidMapCoordinate(x, y) then
        return nil
    end
    return x, y
end

function MapView:CalibrateProtectedCenter(force)
    if not state.mapID or not state.layer then
        return false
    end

    local playerX = Coordinates:GetUnitMapPosition(state.mapID, "player")
    if playerX then
        return false
    end
    if not force and not Addon.Markers:IsPlayerUnderMouse() then
        return false
    end

    local x, y = self:GetCursorMapPosition()
    if not x or not y then
        return false
    end

    Addon.db.protectedFocusByMap[state.mapID] = { x = x, y = y }
    state.focusX = x
    state.focusY = y
    state.panX = 0
    state.panY = 0
    state.followPlayer = true
    state.autoCenterUntil = 0
    state.protectedCenterMode = "calibrado manual"
    state.protectedCenterError = nil
    self:PlaceCanvas()
    Addon.Markers:HighlightPlayer()

    local mapInfo = C_Map.GetMapInfo(state.mapID)
    Addon:Print(Addon:Format("CENTER_SAVED", mapInfo and mapInfo.name or L.THIS_FLOOR))
    return true
end

function MapView:Recenter()
    local playerX, playerY = Coordinates:GetUnitMapPosition(state.mapID, "player")
    if not playerX then
        if IsInstancedContent() and not IsShiftKeyDown() then
            SetProtectedOverviewState()
            if state.mapID then
                Addon.db.zoomByMap[state.mapID] = 1
            end
            self:UpdateGeometry()
            Addon.Markers:HighlightPlayer()
            return
        end

        if self:CalibrateProtectedCenter(IsShiftKeyDown()) then
            return
        end

        local savedX, savedY = GetSavedProtectedFocus(state.mapID)
        if savedX and savedY then
            state.followPlayer = true
            state.focusX = savedX
            state.focusY = savedY
            state.panX = 0
            state.panY = 0
            state.protectedCenterMode = "calibrado guardado"
            self:PlaceCanvas()
            Addon.Markers:HighlightPlayer()
            return
        end

        Addon:Print(L.PROTECTED_POSITION_HELP)
        Addon.Markers:HighlightPlayer()
        return
    end

    self:PrepareForMapChange()
    state.focusX = playerX
    state.focusY = playerY
    self:UpdateGeometry()
    Addon.Markers:HighlightPlayer()
end

function MapView:Initialize(ui)
    mapFrame = ui.mapFrame
    viewport = ui.viewport
    canvas = ui.canvas
    titleText = ui.titleText
    statusText = ui.statusText
    helpText = ui.helpText
end
