-- HousingDecorGuide/Core/HDG_FingerprintMatcher.lua
-- Pure helpers for matching newly-captured rooms to existing internal Room IDs.

local _, privateDB = ...
local M = {}

local TWO_PI = 2 * math.pi
local QUARTER = math.pi / 2

-- WoW housing has an N↔S axis flip between Blizzard's internal compass (the
-- raw radian we get from pin-frame doorFacing) and the player-facing layout
-- view. Blizzard "builds N→S" in data but the editor "displays S→N" on
-- screen, so a door whose data-facing rounds to the 0-radian bucket
-- (compass-N) is the door the player sees on the SOUTH side, and vice versa.
-- We bake the flip in here so every downstream consumer (BFS, orb rendering,
-- fingerprint hashing) speaks player-cardinals.
--
-- Bucket boundaries (radians, mod 2π):
--   data-N (0)    → player "S"
--   data-E (π/2)  → player "E"
--   data-S (π)    → player "N"
--   data-W (3π/2) → player "W"
local DIRS = { "S", "E", "N", "W" }

function M.facingToCardinal(rad)
    if type(rad) ~= "number" then return nil end
    -- Normalize into [0, 2π)
    local r = rad % TWO_PI
    if r < 0 then r = r + TWO_PI end
    -- Round to nearest quarter
    local q = math.floor((r + QUARTER / 2) / QUARTER) % 4
    return DIRS[q + 1]
end

local function _sortedCopy(t)
    local out = {}
    for i = 1, #t do out[i] = t[i] end
    table.sort(out)
    return out
end

function M.computeFingerprint(captured)
    if type(captured) ~= "table" then return nil end
    local doors = {}
    local occupied = {}
    if type(captured.doors) == "table" then
        for _, d in ipairs(captured.doors) do
            local card = M.facingToCardinal(d.facing)
            if card then
                doors[#doors + 1] = card
                if d.occupied then occupied[#occupied + 1] = card end
            end
        end
    end
    return {
        shape = captured.shape,
        degree = #occupied,
        doors = _sortedCopy(doors),
        occupiedDoors = _sortedCopy(occupied),
    }
end

function M.fingerprintHash(fp)
    if type(fp) ~= "table" then return nil end
    return string.format("%s|%d|%s|%s",
        fp.shape or "?",
        fp.degree or 0,
        table.concat(fp.doors or {}, ","),
        table.concat(fp.occupiedDoors or {}, ","))
end

-- Group rooms by fingerprint hash; returns map hash → array
local function _groupByHash(list, getFP)
    local groups = {}
    for _, item in ipairs(list) do
        local fp = getFP(item)
        local h = M.fingerprintHash(fp)
        if h then
            groups[h] = groups[h] or {}
            table.insert(groups[h], item)
        end
    end
    return groups
end

function M.matchRooms(capturedList, existingList)
    local capGroups = _groupByHash(capturedList,
        function(c) return M.computeFingerprint(c) end)
    local exGroups = _groupByHash(existingList,
        function(e) return e.fingerprint end)

    local matched, ambiguous, newRooms, orphans = {}, {}, {}, {}

    -- For each captured group, find the matching existing group by hash
    for hash, capGroup in pairs(capGroups) do
        local exGroup = exGroups[hash] or {}
        if #capGroup == 1 and #exGroup == 1 then
            -- Unambiguous one-to-one
            matched[#matched + 1] = {
                capturedID = capGroup[1].capturedID,
                existingID = exGroup[1].existingID,
            }
        elseif #capGroup > 0 and #exGroup > 0 and (#capGroup > 1 or #exGroup > 1) then
            -- Ambiguous: multiple captured AND/OR multiple existing share fingerprint.
            -- 1-hop neighbor disambiguation is a future enhancement (P2+); for v1
            -- surface every captured in the group as needing manual remap, with all
            -- existing IDs as candidates.
            for _, c in ipairs(capGroup) do
                local candidates = {}
                for _, e in ipairs(exGroup) do candidates[#candidates + 1] = e.existingID end
                ambiguous[#ambiguous + 1] = {
                    capturedID = c.capturedID,
                    candidates = candidates,
                }
            end
        elseif #capGroup > 0 and #exGroup == 0 then
            -- Captured but no existing → new rooms
            for _, c in ipairs(capGroup) do
                newRooms[#newRooms + 1] = { capturedID = c.capturedID }
            end
        end
        exGroups[hash] = nil   -- consumed; remainder = orphans
    end

    -- Anything left in exGroups is an orphan (existing room not seen in this capture)
    for _, exGroup in pairs(exGroups) do
        for _, e in ipairs(exGroup) do
            orphans[#orphans + 1] = { existingID = e.existingID }
        end
    end

    return {
        matched = matched,
        newRooms = newRooms,
        orphans = orphans,
        ambiguous = ambiguous,
    }
end

if privateDB then privateDB.HDG_FingerprintMatcher = M end
return M
