local _, Addon = ...

local Coordinates = {}
Addon.Coordinates = Coordinates
local L = Addon.L

local mapWorldCache = {}
local centerPoint = CreateVector2D(0.5, 0.5)

local function SafeNumber(value)
    if Addon:IsSecret(value) or type(value) ~= "number" then
        return nil
    end
    return value
end

function Coordinates:SafeGetXY(position, allowOutOfBounds)
    if not position or Addon:IsSecret(position) then
        return nil
    end

    local ok, x, y = pcall(function()
        return position:GetXY()
    end)
    if not ok then
        return nil
    end

    x = SafeNumber(x)
    y = SafeNumber(y)
    if not x or not y then
        return nil
    end
    if not allowOutOfBounds and not Addon:IsValidMapCoordinate(x, y) then
        return nil
    end

    return x, y
end

local function GetMapWorldData(mapID)
    if mapWorldCache[mapID] then
        return mapWorldCache[mapID]
    end

    local width, height = C_Map.GetMapWorldSize(mapID)
    width = SafeNumber(width)
    height = SafeNumber(height)
    if not width or not height or width <= 0 or height <= 0 then
        return nil
    end

    local ok, instanceID, center = pcall(C_Map.GetWorldPosFromMapPos, mapID, centerPoint)
    instanceID = SafeNumber(instanceID)
    if not ok or not instanceID or not center then
        return nil
    end

    -- En las coordenadas mundiales de WoW, el primer componente representa
    -- el eje vertical y el segundo el horizontal (la misma convención usada
    -- por Blizzard y HereBeDragons para mapas de instancia).
    local centerY, centerX = Coordinates:SafeGetXY(center, true)
    if not centerX or not centerY then
        return nil
    end

    local data = {
        instanceID = instanceID,
        width = width,
        height = height,
        left = centerX + (width / 2),
        top = centerY + (height / 2),
    }
    mapWorldCache[mapID] = data
    return data
end

local function GetUnitWorldPosition(unit)
    local ok, worldY, worldX, _, instanceID = pcall(UnitPosition, unit)
    if not ok then
        return nil
    end

    worldX = SafeNumber(worldX)
    worldY = SafeNumber(worldY)
    instanceID = SafeNumber(instanceID)
    if not worldX or not worldY or not instanceID then
        return nil
    end

    return worldX, worldY, instanceID
end

local function ConvertWorldPosition(mapID, unit)
    local worldX, worldY, unitInstanceID = GetUnitWorldPosition(unit)
    local mapData = GetMapWorldData(mapID)
    if not worldX or not mapData or unitInstanceID ~= mapData.instanceID then
        return nil
    end

    local x = (mapData.left - worldX) / mapData.width
    local y = (mapData.top - worldY) / mapData.height
    if not Addon:IsValidMapCoordinate(x, y) then
        return nil
    end

    return x, y, "world"
end

function Coordinates:GetUnitMapPosition(mapID, unit)
    if not mapID or not UnitExists(unit) then
        return nil
    end

    local ok, position = pcall(C_Map.GetPlayerMapPosition, mapID, unit)
    if ok then
        local x, y = self:SafeGetXY(position)
        if x and y then
            return x, y, "map"
        end
    end

    -- C_Map.GetPlayerMapPosition suele devolver nil dentro de mazmorras.
    -- UnitPosition continúa disponible, así que convertimos sus coordenadas
    -- mundiales al rectángulo normalizado del piso mostrado.
    return ConvertWorldPosition(mapID, unit)
end

function Coordinates:ResolveMapID()
    local mapID = C_Map.GetBestMapForUnit("player")
    local attempts = 0

    while mapID and mapID > 0 and attempts < 10 do
        if C_Map.MapHasArt(mapID) then
            local layers = C_Map.GetMapArtLayers(mapID)
            if layers and layers[1] then
                return mapID
            end
        end

        local info = C_Map.GetMapInfo(mapID)
        mapID = info and info.parentMapID or nil
        attempts = attempts + 1
    end

    return nil
end

function Coordinates:ClearCache()
    wipe(mapWorldCache)
end

local function DescribeValue(value)
    if Addon:IsSecret(value) then
        return L.DEBUG_PROTECTED
    elseif value == nil then
        return "nil"
    elseif type(value) == "number" then
        return string.format("%.3f", value)
    end
    return tostring(value)
end

function Coordinates:GetDiagnostics(mapID, unit)
    unit = unit or "player"
    local lines = {}
    local mapInfo = mapID and C_Map.GetMapInfo(mapID)
    lines[#lines + 1] = Addon:Format(
        "DEBUG_MAP_INFO",
        DescribeValue(mapID),
        mapInfo and mapInfo.name or "nil",
        mapInfo and DescribeValue(mapInfo.mapType) or "nil"
    )

    local mapOK, mapPosition = pcall(C_Map.GetPlayerMapPosition, mapID, unit)
    local mapX, mapY = mapOK and self:SafeGetXY(mapPosition)
    lines[#lines + 1] = string.format(
        "GetPlayerMapPosition=%s,%s",
        DescribeValue(mapX),
        DescribeValue(mapY)
    )

    local unitOK, unitY, unitX, unitZ, unitInstanceID = pcall(UnitPosition, unit)
    lines[#lines + 1] = Addon:Format(
        "DEBUG_UNIT_POSITION",
        tostring(unitOK),
        DescribeValue(unitX),
        DescribeValue(unitY),
        DescribeValue(unitZ),
        DescribeValue(unitInstanceID)
    )

    local mapData = mapID and GetMapWorldData(mapID)
    lines[#lines + 1] = mapData
        and Addon:Format(
            "DEBUG_WORLD_MAP",
            DescribeValue(mapData.instanceID),
            mapData.width,
            mapData.height
        )
        or L.DEBUG_WORLD_MAP_NIL

    local finalX, finalY, source = self:GetUnitMapPosition(mapID, unit)
    lines[#lines + 1] = Addon:Format(
        "DEBUG_LUA_RESULT",
        DescribeValue(finalX),
        DescribeValue(finalY),
        source or "nil"
    )
    return lines
end
