-- HousingDecorGuide/Core/HDG_RotationDeducer.lua
-- Deduces a room's world rotation from captured pin-frame door facings vs the DB2
-- template.  Returns the rotation R (radians, math-convention CCW) such that
--   (door.facing_local + R) mod 2π  ≈ captured_math_facing
-- for every door.
--
-- Convention bridge:
--   DB2 (facing_local)  — math convention: 0=E, π/2=N, π=W, 3π/2=S (CCW from East)
--   Pin-frame (facing)  — WoW compass:     0=N, π/2=E, π=S, 3π/2=W  (CW from North)
--   Conversion:  math_angle = (π/2 − compass_angle) mod 2π
--
-- Four candidate rotations (quarters of the circle, math-convention CCW):
--   0  = no rotation        (template = world)
--   π/2  = 90° CCW in math  = 90° CW in compass
--   π    = 180°
--   3π/2 = 270° CCW in math = 90° CCW in compass

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

local DEBUG = false   -- set to true in-game to print deduction traces

local PI   = math.pi
local PI_2 = math.pi / 2
local PI32 = 3 * math.pi / 2
local TWO_PI = 2 * math.pi

local CANDIDATE_ROTATIONS = { 0, PI_2, PI, PI32 }

-- NSEW labels under math convention (for Debug and facingsToCardinals)
local MATH_CARDINAL = {}
MATH_CARDINAL[0]    = "E"
MATH_CARDINAL[1]    = "N"   -- π/2 bucket
MATH_CARDINAL[2]    = "W"   -- π bucket
MATH_CARDINAL[3]    = "S"   -- 3π/2 bucket

local COMPASS_CARDINAL = { "N", "E", "S", "W" }   -- bucket 0..3

-- Convert a WoW compass-bearing radian to math-convention radian.
local function _compassToMath(compass)
    return (PI_2 - compass) % TWO_PI
end

-- Round a math-convention radian to the nearest quarter (0, π/2, π, 3π/2).
local function _roundQuarter(r)
    local r2 = r % TWO_PI
    -- bucket index 0..3: multiply by 2/π, round, mod 4
    local bucket = math.floor(r2 / PI_2 + 0.5) % 4
    return bucket * PI_2
end

-- Build an unordered-set key from a list of rounded math facings.
-- Sort so order doesn't matter.
local function _facingSetKey(facings)
    local rounded = {}
    for i, f in ipairs(facings) do
        rounded[i] = string.format("%.4f", _roundQuarter(f))
    end
    table.sort(rounded)
    return table.concat(rounded, "|")
end

-- ---------------------------------------------------------------------------
-- Public API
-- ---------------------------------------------------------------------------

--- deduce(shapeID, capturedCompassFacings) → rotation (radians) | nil, reason
-- shapeID               — string key in HDG_RoomShapesDB (e.g. "l_shape")
-- capturedCompassFacings — array of raw radian values from pin-frame doorFacing
--                          (WoW compass convention: 0=N CW)
-- Returns:
--   rotation  — one of {0, π/2, π, 3π/2} (math-convention CCW from East)
--   nil, "doors_count_mismatch"  — captured door count ≠ DB2 door count
--   nil, "facing_mismatch"       — no rotation candidate produced a matching set
function M.deduce(shapeID, capturedCompassFacings)
    local DB = privateDB and privateDB.HDG_RoomShapesDB
    if not DB then
        -- Standalone/test mode: try to load via dofile
        ---@diagnostic disable-next-line: undefined-global
        DB = (package and dofile) and dofile("HDG_RoomShapesDB.lua") or nil
    end
    if not DB then return nil, "no_db" end

    local shape = DB.GetShape(shapeID)
    if not shape then return nil, "unknown_shape" end

    local dbDoors = shape.doors
    if #dbDoors ~= #capturedCompassFacings then
        return nil, "doors_count_mismatch"
    end

    -- Convert captured facings to math convention
    local capturedMath = {}
    for i, cf in ipairs(capturedCompassFacings) do
        capturedMath[i] = _compassToMath(cf)
    end
    local capturedKey = _facingSetKey(capturedMath)

    if DEBUG then
        print(string.format("[RotationDeducer] shape=%s captured_math_key=%s", shapeID, capturedKey))
    end

    -- Try each candidate rotation
    for _, r in ipairs(CANDIDATE_ROTATIONS) do
        local rotated = {}
        for i, door in ipairs(dbDoors) do
            rotated[i] = (door.facing_local + r) % TWO_PI
        end
        local rotatedKey = _facingSetKey(rotated)

        if DEBUG then
            print(string.format("  r=%.4f rotated_key=%s match=%s", r, rotatedKey, tostring(rotatedKey == capturedKey)))
        end

        if rotatedKey == capturedKey then
            return r
        end
    end

    return nil, "facing_mismatch"
end

--- facingsToCardinals(shapeID, rotation, capturedCompassFacings, convention)
-- Returns a sorted array of compass cardinal strings {"N","E","S","W"} for each
-- door in the given shape at the given world rotation.
-- Uses DB2 door list (not captured list) so the order is stable.
-- convention: "compass" (default) → output cardinal in WoW compass space
--             "math"             → output cardinal in math space (for debugging)
function M.facingsToCardinals(shapeID, rotation, convention)
    convention = convention or "compass"
    local DB = privateDB and privateDB.HDG_RoomShapesDB
    if not DB then return {} end
    local shape = DB.GetShape(shapeID)
    if not shape then return {} end

    local result = {}
    for _, door in ipairs(shape.doors) do
        local mathFacing = (door.facing_local + (rotation or 0)) % TWO_PI
        local card
        if convention == "math" then
            local bucket = math.floor(mathFacing / PI_2 + 0.5) % 4
            card = MATH_CARDINAL[bucket]
        else
            -- Convert math → compass for output cardinal
            local compassFacing = (PI_2 - mathFacing) % TWO_PI
            local q = math.floor((compassFacing + PI_2 / 2) / PI_2) % 4
            card = COMPASS_CARDINAL[q + 1]
        end
        result[#result + 1] = card
    end
    return result
end

--- compassToMath(rad) — exposed for tests
function M.compassToMath(rad)
    return _compassToMath(rad)
end

--- roundQuarter(rad) — exposed for tests
function M.roundQuarter(rad)
    return _roundQuarter(rad)
end

if privateDB then privateDB.HDG_RotationDeducer = M end
return M
