local addonName, ZL = ...

-- The old-world continent hierarchy also contains later-expansion and
-- instance maps. Vanilla therefore uses an explicit outdoor allow-list;
-- expansion ownership must never be inferred merely from physical ancestry.
ZL.Expansions = {
    {
        key = "vanilla", name = "Vanilla", roots = { 12, 13 },
        continentZones = {
            [12] = { 1, 7, 10, 57, 62, 63, 64, 65, 66, 69, 70, 71, 76, 77, 78, 80, 81, 83, 97, 106, 174, 194, 199 },
            [13] = { 14, 15, 17, 18, 21, 22, 23, 25, 26, 27, 32, 36, 37, 42, 47, 48, 49, 50, 51, 52, 56, 94, 95, 179, 210 },
        },
    },
    { key = "tbc", name = "The Burning Crusade", roots = { 101 }, entryZones = { 100 } },
    { key = "wrath", name = "Wrath of the Lich King", roots = { 113 }, entryZones = { 114, 117 } },
    { key = "cataclysm", name = "Cataclysm", zones = { 198, 203, 204, 205, 207, 241, 244, 245, 249 } },
    { key = "mists", name = "Mists of Pandaria", roots = { 424 }, entryZones = { 371 } },
    { key = "warlords", name = "Warlords of Draenor", roots = { 572 }, entryZones = { 525, 539 } },
    { key = "legion", name = "Legion", roots = { 619 } },
    { key = "bfa", name = "Battle for Azeroth", roots = { 875, 876 }, entryZones = { 862, 895 } },
    { key = "shadowlands", name = "Shadowlands", roots = { 1550 }, entryZones = { 1533 } },
    { key = "dragonflight", name = "Dragonflight", roots = { 1978 }, entryZones = { 2022 } },
    { key = "warwithin", name = "The War Within", roots = { 2274 }, entryZones = { 2248 } },
}

-- Ordinary faction starting zones can be selected by any race of that
-- faction. Instanced racial introductions are restricted to their race.
ZL.StartingZones = {
    { mapID = 37, faction = "Alliance", continentID = 13, expansionKey = "vanilla" },
    { mapID = 27, faction = "Alliance", continentID = 13, expansionKey = "vanilla" },
    { mapID = 57, faction = "Alliance", continentID = 12, expansionKey = "vanilla" },
    { mapID = 97, faction = "Alliance", continentID = 12, expansionKey = "vanilla" },
    { mapID = 179, faction = "Alliance", continentID = 13, expansionKey = "vanilla", races = { 22 } }, -- Worgen
    { mapID = 1, faction = "Horde", continentID = 12, expansionKey = "vanilla" },
    { mapID = 7, faction = "Horde", continentID = 12, expansionKey = "vanilla" },
    { mapID = 18, faction = "Horde", continentID = 13, expansionKey = "vanilla" },
    { mapID = 94, faction = "Horde", continentID = 13, expansionKey = "vanilla" },
    { mapID = 194, faction = "Horde", continentID = 12, expansionKey = "vanilla", races = { 9 } }, -- Goblin
    { mapID = 378, faction = "Neutral", expansionKey = "vanilla", races = { 24, 25, 26 } }, -- Pandaren
    { mapID = 1409, faction = "Neutral", expansionKey = "vanilla", currentMapOnly = true }, -- Exile's Reach
    { mapID = 2107, faction = "Neutral", expansionKey = "dragonflight", races = { 52, 70 } }, -- Dracthyr
}

-- Faction questing territories are excluded from the opposite faction's
-- challenge pool. Contested outdoor zones remain shared.
ZL.ZoneEligibility = {
    [1] = { faction = "Horde" }, [7] = { faction = "Horde" }, [18] = { faction = "Horde" },
    [21] = { faction = "Horde" }, [76] = { faction = "Horde" }, [94] = { faction = "Horde" },
    [95] = { faction = "Horde" }, [174] = { faction = "Horde", races = { 9 } },
    [194] = { faction = "Horde", races = { 9 } },
    [27] = { faction = "Alliance" }, [37] = { faction = "Alliance" }, [48] = { faction = "Alliance" },
    [49] = { faction = "Alliance" }, [52] = { faction = "Alliance" }, [57] = { faction = "Alliance" },
    [62] = { faction = "Alliance" }, [97] = { faction = "Alliance" }, [106] = { faction = "Alliance" },
    [179] = { faction = "Alliance", races = { 22 } },
    [378] = { races = { 24, 25, 26 } }, [1409] = { currentMapOnly = true },
    [2107] = { races = { 52, 70 } },
}

local function Contains(values, wanted)
    if not values then return true end
    for _, value in ipairs(values) do if value == wanted then return true end end
    return false
end

function ZL:IsZoneEligibleForCharacter(mapID)
    local rule = self.ZoneEligibility[mapID]
    if not rule then return true end
    local playerFaction = UnitFactionGroup("player")
    if rule.faction and rule.faction ~= "Neutral" and rule.faction ~= playerFaction then return false end
    local _, _, raceID = UnitRace("player")
    if not Contains(rule.races, raceID) then return false end
    if rule.currentMapOnly and self:GetCurrentZoneID() ~= mapID then return false end
    return true
end

function ZL:IsStartingZoneEligible(starter, continentID, expansionKey)
    if starter.expansionKey ~= expansionKey then return false end
    if starter.continentID and starter.continentID ~= continentID then return false end
    local faction = UnitFactionGroup("player")
    if starter.faction ~= "Neutral" and starter.faction ~= faction then return false end
    local _, _, raceID = UnitRace("player")
    if not Contains(starter.races, raceID) then return false end
    if starter.currentMapOnly and self:GetCurrentZoneID() ~= starter.mapID then return false end
    return self:IsZoneEligibleForCharacter(starter.mapID)
end

function ZL:RefreshExpansionMaps()
    self.ExpansionByMap, self.ExpansionByKey = {}, {}
    for _, expansion in ipairs(self.Expansions) do
        self.ExpansionByKey[expansion.key] = expansion
        for _, rootMapID in ipairs(expansion.roots or {}) do
            self.ExpansionByMap[rootMapID] = expansion.key
            if not expansion.continentZones then
                local children = C_Map.GetMapChildrenInfo(rootMapID, Enum and Enum.UIMapType and Enum.UIMapType.Zone, true) or {}
                for _, child in ipairs(children) do self.ExpansionByMap[child.mapID] = expansion.key end
            end
        end
        for _, zones in pairs(expansion.continentZones or {}) do
            for _, mapID in ipairs(zones) do self.ExpansionByMap[mapID] = expansion.key end
        end
        for _, mapID in ipairs(expansion.zones or {}) do self.ExpansionByMap[mapID] = expansion.key end
    end
    for _, starter in ipairs(self.StartingZones) do self.ExpansionByMap[starter.mapID] = starter.expansionKey end
end

function ZL:GetMapExpansionKey(mapID)
    if not mapID then return nil end
    if not self.ExpansionByMap then self:RefreshExpansionMaps() end
    if self.ExpansionByMap[mapID] then return self.ExpansionByMap[mapID] end
    local candidate = mapID
    while candidate do
        local info = C_Map.GetMapInfo(candidate)
        candidate = info and info.parentMapID
        local expansionKey = candidate and self.ExpansionByMap[candidate]
        if expansionKey then
            local expansion = self.ExpansionByKey[expansionKey]
            -- Never inherit Vanilla from an old-world root: only maps in its
            -- explicit allow-list belong to that content pack.
            if expansion and expansion.continentZones then return nil end
            return expansionKey
        end
    end
end

function ZL:IsExpansionEnabled(key)
    return key and ZL_Saved.settings.expansions[key] == true or false
end

function ZL:IsMapExpansionEnabled(mapID)
    local key = self:GetMapExpansionKey(mapID)
    return key and self:IsExpansionEnabled(key) or false
end

function ZL:SetExpansionEnabled(key, enabled)
    if not self.ExpansionByKey then self:RefreshExpansionMaps() end
    if not self.ExpansionByKey[key] then return end
    ZL_Saved.settings.expansions[key] = enabled and true or false
    self.DynamicAdjacency, self.MapRasterCache = {}, {}
    self._uiSignature = nil
    self:CheckZoneState()
    self:RefreshUI()
end

function ZL:GetExpansionContinents(key)
    local expansion = self.ExpansionByKey and self.ExpansionByKey[key]
    if not expansion then return {} end
    local continents = {}
    for _, rootMapID in ipairs(expansion.roots or {}) do continents[#continents + 1] = rootMapID end
    if #continents == 0 and #(expansion.zones or {}) > 0 then
        for index, candidate in ipairs(self.Expansions) do
            if candidate.key == key then continents[1] = -index; break end
        end
    end
    return continents
end

function ZL:GetContinentName(continentID, expansionKey)
    local info = continentID and continentID > 0 and C_Map.GetMapInfo(continentID)
    local expansion = self.ExpansionByKey and self.ExpansionByKey[expansionKey]
    return (info and info.name) or (expansion and expansion.name) or "World"
end

function ZL:GetMapContinentID(mapID)
    local expansionKey = self:GetMapExpansionKey(mapID)
    local expansion = expansionKey and self.ExpansionByKey and self.ExpansionByKey[expansionKey]
    if not expansion then return nil, expansionKey end
    if expansion.continentZones then
        for continentID, zones in pairs(expansion.continentZones) do
            for _, zoneMapID in ipairs(zones) do
                if zoneMapID == mapID then return continentID, expansionKey end
            end
        end
    end
    local roots = {}
    for _, rootMapID in ipairs(expansion.roots or {}) do roots[rootMapID] = true end
    local candidate = mapID
    while candidate do
        if roots[candidate] then return candidate, expansionKey end
        local info = C_Map.GetMapInfo(candidate)
        candidate = info and info.parentMapID
    end
    return self:GetExpansionContinents(expansionKey)[1], expansionKey
end

function ZL:GetContinentZoneIDs(continentID, expansionKey, includeIneligible)
    expansionKey = expansionKey or select(2, self:GetMapContinentID(continentID))
    local expansion = expansionKey and self.ExpansionByKey and self.ExpansionByKey[expansionKey]
    local zones = {}
    if not expansion or not self:IsExpansionEnabled(expansionKey) then return zones end
    local source = expansion.continentZones and expansion.continentZones[continentID]
    if source then
        for _, mapID in ipairs(source) do
            if includeIneligible or self:IsZoneEligibleForCharacter(mapID) then zones[#zones + 1] = mapID end
        end
    elseif continentID and continentID > 0 then
        local children = C_Map.GetMapChildrenInfo(continentID, Enum and Enum.UIMapType and Enum.UIMapType.Zone, true) or {}
        for _, child in ipairs(children) do
            if self:GetMapExpansionKey(child.mapID) == expansionKey and not self:IsHubMap(child.mapID) and
                not self:GetInstanceRouteZone(child.mapID) and (includeIneligible or self:IsZoneEligibleForCharacter(child.mapID)) then
                zones[#zones + 1] = child.mapID
            end
        end
    else
        for _, mapID in ipairs(expansion.zones or {}) do
            if not self:IsHubMap(mapID) and (includeIneligible or self:IsZoneEligibleForCharacter(mapID)) then
                zones[#zones + 1] = mapID
            end
        end
    end
    table.sort(zones, function(a, b) return self:GetZoneDefinition(a).name < self:GetZoneDefinition(b).name end)
    return zones
end

function ZL:GetSelectableContinents()
    local result = {}
    for _, expansion in ipairs(self.Expansions) do
        if self:IsExpansionEnabled(expansion.key) then
            for _, continentID in ipairs(self:GetExpansionContinents(expansion.key)) do
                if #self:GetContinentZoneIDs(continentID, expansion.key) > 0 then
                    result[#result + 1] = {
                        continentID = continentID, expansionKey = expansion.key,
                        name = self:GetContinentName(continentID, expansion.key), expansionName = expansion.name,
                    }
                end
            end
        end
    end
    return result
end

function ZL:GetExpansionNameForMap(mapID)
    local key = self:GetMapExpansionKey(mapID)
    local expansion = key and self.ExpansionByKey and self.ExpansionByKey[key]
    return expansion and expansion.name, key
end

function ZL:GetExpansionZoneIDs(key)
    local zones = {}
    for _, continentID in ipairs(self:GetExpansionContinents(key)) do
        for _, mapID in ipairs(self:GetContinentZoneIDs(continentID, key)) do zones[#zones + 1] = mapID end
    end
    return zones
end

function ZL:GetAdjacentZoneIDs(mapID)
    local continentID, expansionKey = self:GetMapContinentID(mapID)
    self.DynamicAdjacency = self.DynamicAdjacency or {}
    if continentID and continentID > 0 and not self.DynamicAdjacency[continentID] then
        local graph, zoneSet = {}, {}
        for _, zoneMapID in ipairs(self:GetContinentZoneIDs(continentID, expansionKey)) do
            zoneSet[zoneMapID], graph[zoneMapID] = true, {}
        end
        local function Normalize(candidate)
            while candidate do
                if zoneSet[candidate] then return candidate end
                local info = C_Map.GetMapInfo(candidate)
                candidate = info and info.parentMapID
            end
        end
        local function Link(a, b)
            if a and b and a ~= b then graph[a][b], graph[b][a] = true, true end
        end
        local columns, rows, previousRow = 72, 48, {}
        for row = 0, rows - 1 do
            local currentRow, left = {}, nil
            for column = 0, columns - 1 do
                local info = C_Map.GetMapInfoAtPosition and
                    C_Map.GetMapInfoAtPosition(continentID, (column + 0.5) / columns, (row + 0.5) / rows)
                local zoneMapID = Normalize(info and info.mapID)
                currentRow[column] = zoneMapID
                Link(left, zoneMapID)
                Link(previousRow[column], zoneMapID)
                left = zoneMapID
            end
            previousRow = currentRow
        end
        self.DynamicAdjacency[continentID] = graph
    end
    local combined = {}
    for _, adjacentMapID in ipairs(self.Adjacency[mapID] or {}) do
        if self:IsZoneEligibleForCharacter(adjacentMapID) and self:IsMapExpansionEnabled(adjacentMapID) then
            combined[adjacentMapID] = true
        end
    end
    local graph = continentID and self.DynamicAdjacency[continentID]
    for adjacentMapID in pairs(graph and graph[mapID] or {}) do combined[adjacentMapID] = true end
    local result = {}
    for adjacentMapID in pairs(combined) do result[#result + 1] = adjacentMapID end
    table.sort(result, function(a, b) return self:GetZoneDefinition(a).name < self:GetZoneDefinition(b).name end)
    return result
end
