-- VDS_HarmonyWheel
-- ============================================================================
-- Pure group-level colour-wheel resolver. Answers "which colour FAMILY is the
-- complement / analogous / triad / split / clash of this family" on the RYB
-- (Itten) wheel -- NOT per-dye hue (per-dye hue is the signal we distrust; see
-- docs/VDS_AutoHarmonise_Design.md 3.2). The per-dye pick happens in the engine
-- selector. Neutrals (Brown/Grey/Black/White) carry no wheel angle. Lua 5.1.

VDS = VDS or {}
VDS.HarmonyWheel = VDS.HarmonyWheel or {}
local HW = VDS.HarmonyWheel

-- Fixed iteration order -> deterministic tie-breaking in nearest().
HW.CHROMATIC = { "Red", "Orange", "Yellow", "Green", "Teal", "Blue", "Purple", "Pink" }
HW.ANGLE     = { Red = 0, Orange = 60, Yellow = 120, Green = 180,
                 Teal = 210, Blue = 240, Purple = 285, Pink = 330 }
HW.NEUTRALS  = { Brown = true, Grey = true, White = true, Black = true }

local function adist(a, b)
    local d = math.abs((a - b) % 360)
    return math.min(d, 360 - d)
end

local function nearest(angle, exclude)
    local best, bestd
    for _, g in ipairs(HW.CHROMATIC) do
        if not (exclude and exclude[g]) then
            local d = adist(angle, HW.ANGLE[g])
            if not bestd or d < bestd then best, bestd = g, d end
        end
    end
    return best
end

-- relationship -> list of target chromatic groups.
-- "Neutral" and "Monochromatic" are not hue relationships -- the engine handles
-- them from the neutral pool / the anchor's own group, so they return {} here.
function HW.targets(group, rel)
    local t = HW.ANGLE[group]
    if not t then return {} end  -- exception(nullable): neutral anchor has no wheel angle
    local ex = { [group] = true }
    if rel == "Complement" then
        return { nearest(t + 180, ex) }
    elseif rel == "Analogous" then
        return { nearest(t - 40, ex), nearest(t + 40, ex) }
    elseif rel == "Triad" then
        return { nearest(t - 120, ex), nearest(t + 120, ex) }
    elseif rel == "Split" then
        return { nearest(t + 140, ex), nearest(t + 220, ex) }
    elseif rel == "Clash" then
        -- Discordant = the chromatic family FARTHEST from the anchor that still
        -- has NO harmonious relationship to it (not analogous / complement /
        -- triad / split). The orphan set is the genuinely unrelated hues; taking
        -- the farthest gives the loudest warm<->cool clash. Excluding every
        -- harmonious set guarantees Clash can never collide with Triad or Split.
        -- (The old t+150 landed on a triad member, so Clash == Triad.)
        local used = { [group] = true }
        local function mark(list) for _, g in ipairs(list) do used[g] = true end end
        mark(HW.targets(group, "Analogous"))
        mark(HW.targets(group, "Complement"))
        mark(HW.targets(group, "Triad"))
        mark(HW.targets(group, "Split"))
        local best, bestd
        for _, g in ipairs(HW.CHROMATIC) do
            if not used[g] then
                local d = adist(t, HW.ANGLE[g])
                if not bestd or d > bestd then best, bestd = g, d end
            end
        end
        return best and { best } or {}
    end
    return {}
end

-- Deployment proportion-bar ratios for the tooltips (decor reading: loud -> small).
-- Keys are the internal relationship names (UI maps "Complement"->label etc).
-- Each list = [anchor share, suggestion1, suggestion2...]; one entry per chip
-- the relationship shows. buildProportionBar normalizes to whatever picks exist.
HW.BAR = {
    Complement    = { 75, 25 },
    Analogous     = { 50, 30, 20 },
    Neutral       = { 28, 40, 32 },   -- anchor is the feature; the two neutrals ground the larger area
    Monochromatic = { 50, 30, 20 },
    Clash         = { 78, 22 },
    Triad         = { 55, 28, 17 },
    Split         = { 55, 28, 17 },
    Tones         = { 40, 32, 28 },   -- tonal neutral palette, fairly even
    Contrast      = { 55, 45 },       -- warm <-> cool neutral, near-balanced
    Accent        = { 60, 25, 15 },   -- neutral anchor grounds; accents are small pops
}
