-------------------------------------------------------
-- Moonlight_SkillSound
-- 스킬 사용 시 지정된 사운드를 재생하는 애드온
-------------------------------------------------------

local ADDON_NAME = "Moonlight_SkillSound"

-------------------------------------------------------
-- 직업 정의
-------------------------------------------------------
local CLASS_LIST = {
    { id = 1,  name = "전사",        nameEN = "WARRIOR",      color = "C79C6E" },
    { id = 2,  name = "성기사",      nameEN = "PALADIN",      color = "F58CBA" },
    { id = 3,  name = "사냥꾼",      nameEN = "HUNTER",       color = "ABD473" },
    { id = 4,  name = "도적",        nameEN = "ROGUE",        color = "FFF569" },
    { id = 5,  name = "사제",        nameEN = "PRIEST",       color = "FFFFFF" },
    { id = 6,  name = "죽음의 기사", nameEN = "DEATHKNIGHT",  color = "C41F3B" },
    { id = 7,  name = "주술사",      nameEN = "SHAMAN",       color = "0070DE" },
    { id = 8,  name = "마법사",      nameEN = "MAGE",         color = "69CCF0" },
    { id = 9,  name = "흑마법사",    nameEN = "WARLOCK",      color = "9482C9" },
    { id = 10, name = "수도사",      nameEN = "MONK",         color = "00FF96" },
    { id = 11, name = "드루이드",    nameEN = "DRUID",        color = "FF7D0A" },
    { id = 12, name = "악마사냥꾼",  nameEN = "DEMONHUNTER",  color = "A330C9" },
    { id = 13, name = "기원사",      nameEN = "EVOKER",       color = "33937F" },
    { id = 14, name = "장신구",      nameEN = "TRINKET",      color = "E8D000" },
}

-- ★ 직업 역방향 맵: classFile(영문) → CLASS_LIST 항목
--   RebuildRuntimeMap에서 매번 순회하지 않도록 미리 구성
local CLASS_BY_ENGLISH = {}
for _, c in ipairs(CLASS_LIST) do
    CLASS_BY_ENGLISH[c.nameEN] = c
end

local CHANNELS = { "Master", "SFX", "Music", "Ambience", "Dialog" }

-- ★ soundChannel 정규화: 대소문자 혼용 방지 ("master"→"Master" 등)
local CHANNEL_NORMALIZE = {}
for _, ch in ipairs(CHANNELS) do
    CHANNEL_NORMALIZE[ch:lower()] = ch
end
local function NormalizeChannel(ch)
    return CHANNEL_NORMALIZE[(ch or ""):lower()] or "Master"
end

-------------------------------------------------------
-- DB
-------------------------------------------------------
MoonlightSkillSoundDB = MoonlightSkillSoundDB or {}

-------------------------------------------------------
-- 기본값 적용 (DefaultPreset.lua 의 값을 DB에 머지)
-- overwrite=true  → 기존 항목도 덮어씀 (첫 설치)
-- overwrite=false → 없는 항목만 추가   (버전 업데이트)
-------------------------------------------------------
local function ApplyMissingDefaults(overwrite)
    if not MoonlightSkillSoundDefaults then return end
    for classID, spells in pairs(MoonlightSkillSoundDefaults) do
        if not MoonlightSkillSoundDB[classID] then
            MoonlightSkillSoundDB[classID] = {}
        end
        for spellID, data in pairs(spells) do
            if overwrite or not MoonlightSkillSoundDB[classID][spellID] then
                MoonlightSkillSoundDB[classID][spellID] = {
                    soundPath      = data.soundPath,
                    soundChannel   = NormalizeChannel(data.soundChannel),
                    enabled        = data.enabled ~= false,
                    triggerType    = data.triggerType or "instant",
                    -- 이미지 필드
                    imagePath        = data.imagePath        or "",
                    imageEnabled     = data.imageEnabled     == true,
                    imageDuration    = data.imageDuration    or 3.0,
                    imageScale       = data.imageScale       or 1.0,
                    imagePosX        = data.imagePosX        or 0,
                    imagePosY        = data.imagePosY        or 0,
                    imagePosAnchor   = data.imagePosAnchor   or nil,
                    -- 페이드 효과
                    imageFadeEnabled = data.imageFadeEnabled == true,
                    imageFadeIn      = data.imageFadeIn      or 0.3,
                    imageFadeOut     = data.imageFadeOut     or 0.4,

                    -- 초시계 필드
                    timerEnabled   = data.timerEnabled   == true,
                    timerDuration  = data.timerDuration  or 0,
                    timerSpellName = data.timerSpellName or "",
                    timerFontSize  = data.timerFontSize  or 22,
                    timerColorR    = data.timerColorR    or 1,
                    timerColorG    = data.timerColorG    or 1,
                    timerColorB    = data.timerColorB    or 1,
                    timerBgEnabled = data.timerBgEnabled == true,
                    timerBgAlpha   = data.timerBgAlpha   or 0.5,
                    timerPosX      = data.timerPosX      or 0,
                    timerPosY      = data.timerPosY      or 0,
                    timerPosAnchor = data.timerPosAnchor or nil,
                }
            end
        end
    end
end

local function InitDB()
    for _, c in ipairs(CLASS_LIST) do
        if not MoonlightSkillSoundDB[c.id] then
            MoonlightSkillSoundDB[c.id] = {}
        end
    end

    local isFirstInstall = (MoonlightSkillSoundDB._version == nil)
    local PRESET_VERSION = MOONLIGHT_SKILLSOUND_PRESET_VERSION or 1

    if isFirstInstall then
        ApplyMissingDefaults(true)
        MoonlightSkillSoundDB._version = PRESET_VERSION
    elseif (MoonlightSkillSoundDB._version or 0) < PRESET_VERSION then
        ApplyMissingDefaults(false)
        MoonlightSkillSoundDB._version = PRESET_VERSION
    end

    -- 전역 초시계 앵커 기본값
    if not MoonlightSkillSoundDB._timerAnchor then
        MoonlightSkillSoundDB._timerAnchor = {
            posX    = 0,
            posY    = 150,
            anchor  = nil,   -- nil = CENTER 기준, "BOTTOMLEFT" = 좌하단 기준
            gap     = 36,    -- 초시계 간격(px)
            growDir = "down", -- "down" = 아래로 쌓임, "up" = 위로 쌓임
        }
    end
    if MoonlightSkillSoundDB._timerAnchor.gap     == nil then MoonlightSkillSoundDB._timerAnchor.gap     = 36     end
    if MoonlightSkillSoundDB._timerAnchor.growDir == nil then MoonlightSkillSoundDB._timerAnchor.growDir = "down" end
end

-------------------------------------------------------
-- 런타임 맵 (triggerType별 분리)
-------------------------------------------------------
local instantMap = {}
local castMap    = {}
local channelMap = {}

-- ────────────────────────────────────────────────────────────
--  이미지 맵 (RebuildRuntimeMap보다 먼저 정의해야 호출 가능)
-- ────────────────────────────────────────────────────────────
local imageMap   = {}
local imgMoveMode = false

local imgFrame = CreateFrame("Frame", "MoonlightSkillImageFrame", UIParent)
imgFrame:SetSize(256, 256)
imgFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
imgFrame:Hide()
-- 현재 배치된 CENTER 기준 오프셋 (확대 페이드아웃 시 제자리 유지에 사용)
imgFrame._centerX = 0
imgFrame._centerY = 0

local imgTexture = imgFrame:CreateTexture(nil, "OVERLAY")
imgTexture:SetAllPoints(imgFrame)
imgTexture:SetAlpha(0)

local imgHideTimer  = nil
local imgFadeTicker = nil

local function ImageFadeOut(fadeTime)
    if imgFadeTicker then imgFadeTicker:Cancel() end
    local start = GetTime()
    local FADE  = math.max(0.05, fadeTime or 0.4)
    imgFadeTicker = C_Timer.NewTicker(0.05, function(t)  -- 20fps로 충분
        local prog = math.min((GetTime() - start) / FADE, 1)
        imgTexture:SetAlpha(1 - prog)
        if prog >= 1 then
            imgFrame:Hide()
            imgTexture:SetAlpha(0)
            t:Cancel()
            imgFadeTicker = nil
        end
    end)
end

local function ShowSkillImage(spellID)
    if imgMoveMode then return end
    local entry = imageMap[tonumber(spellID)] or imageMap[spellID]
    if not entry then return end

    -- 타이머/애니메이션 초기화
    if imgHideTimer  then imgHideTimer:Cancel();  imgHideTimer  = nil end
    if imgFadeTicker then imgFadeTicker:Cancel(); imgFadeTicker = nil end

    -- 최종 목적지 좌표
    local destX = entry.posX or 0
    local destY = entry.posY or 0

    local function applyFinalPos()
        imgFrame:ClearAllPoints()
        if entry.posAnchor == "BOTTOMLEFT" then
            imgFrame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", destX, destY)
        else
            imgFrame:SetPoint("CENTER", UIParent, "CENTER", destX, destY)
        end
    end

    imgFrame:SetSize(entry.width or 256, entry.height or 256)
    imgFrame:SetScale(entry.scale or 1.0)
    imgTexture:SetTexture(entry.path)

    applyFinalPos()
    imgFrame:Show()

    -- ── 페이드인 ─────────────────────────────────────────────
    if entry.fadeEnabled then
        local fadeIn = math.max(0.05, entry.fadeIn or 0.3)
        imgTexture:SetAlpha(0)
        local startFade = GetTime()
        imgFadeTicker = C_Timer.NewTicker(0.05, function(t)  -- 20fps로 충분
            local prog = math.min((GetTime() - startFade) / fadeIn, 1)
            imgTexture:SetAlpha(prog)
            if prog >= 1 then
                imgTexture:SetAlpha(1)
                t:Cancel()
                imgFadeTicker = nil
            end
        end)
    else
        imgTexture:SetAlpha(1)
    end

    -- ── 표시 시간 후 페이드아웃 또는 즉시 숨김 ───────────────
    imgHideTimer = C_Timer.NewTimer(entry.duration, function()
        imgHideTimer = nil
        if not imgMoveMode then
            if entry.fadeEnabled then
                ImageFadeOut(entry.fadeOut)
            else
                imgFrame:Hide()
                imgTexture:SetAlpha(0)
            end
        end
    end)
end

local function RebuildImageMap()
    wipe(imageMap)
    local _, classFile = UnitClass("player")
    if not classFile then return end
    local cls = CLASS_BY_ENGLISH[classFile]
    if not cls then return end
    local function loadImg(tbl)
        if not tbl then return end
        for spellID, data in pairs(tbl) do
            if data.imageEnabled and data.imagePath and data.imagePath ~= "" then
                imageMap[tonumber(spellID) or spellID] = {
                    path         = data.imagePath,
                    duration     = data.imageDuration or 3.0,
                    scale        = data.imageScale    or 1.0,
                    width        = 256,
                    height       = 256,
                    posX         = data.imagePosX     or 0,
                    posY         = data.imagePosY     or 0,
                    posAnchor    = data.imagePosAnchor,
                    fadeEnabled  = data.imageFadeEnabled == true,
                    fadeIn       = data.imageFadeIn    or 0.3,
                    fadeOut      = data.imageFadeOut   or 0.4,

                }
            end
        end
    end
    loadImg(MoonlightSkillSoundDB[cls.id])
    loadImg(MoonlightSkillSoundDB[14])
end

-- ────────────────────────────────────────────────────────────
--  초시계 런타임 (앵커 기준 스택 방식)
--  모든 초시계는 전역 앵커(MoonlightSkillSoundDB._timerAnchor) 기준으로
--  위에서부터 gap 간격으로 쌓임.
-- ────────────────────────────────────────────────────────────
local timerMap      = {}
local timerMoveMode = false   -- 앵커 이동 모드

-- 활성 타이머: spellID → { frame, ticker }
local activeTimers  = {}

-- 프레임 풀 (재사용)
local timerFramePool = {}

local function GetTimerAnchor()
    if MoonlightSkillSoundDB and MoonlightSkillSoundDB._timerAnchor then
        return MoonlightSkillSoundDB._timerAnchor
    end
    return { posX=0, posY=150, anchor=nil, gap=36 }
end

local function GetPooledTimerFrame()
    for _, f in ipairs(timerFramePool) do
        if not f:IsShown() then return f end
    end
    local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    f:SetSize(200, 36)
    f:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
    })
    f:SetBackdropColor(0,0,0,0)
    f:SetBackdropBorderColor(0,0,0,0)
    f:Hide()
    f:SetAlpha(0)
    local t = f:CreateFontString(nil, "OVERLAY")
    t:SetFont(GameFontNormal:GetFont(), 22, "OUTLINE")
    t:SetPoint("CENTER", f, "CENTER", 0, 0)
    t:SetText("")
    t:SetTextColor(1,1,1)
    f.timerText = t
    table.insert(timerFramePool, f)
    return f
end

-- 모든 활성 초시계의 위치를 앵커 기준으로 재배치
local function RelayoutTimers()
    local anc     = GetTimerAnchor()
    local gap     = anc.gap     or 36
    local growDir = anc.growDir or "down"
    local dir     = (growDir == "up") and 1 or -1   -- 위 = +Y, 아래 = -Y

    -- spellID 정렬로 순서 일관성 유지
    local order = {}
    for sid in pairs(activeTimers) do table.insert(order, sid) end
    table.sort(order)

    for i, sid in ipairs(order) do
        local info = activeTimers[sid]
        if info and info.frame and info.frame:IsShown() then
            local f       = info.frame
            local offsetY = dir * (i - 1) * gap

            f:ClearAllPoints()
            if anc.anchor == "BOTTOMLEFT" then
                f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", anc.posX, anc.posY + offsetY)
            else
                f:SetPoint("CENTER", UIParent, "CENTER", anc.posX, anc.posY + offsetY)
            end

            -- 첫 번째(앵커 기준) 스킬: 텍스트에 ★ 마커 추가
            info.isAnchor = (i == 1)
        end
    end
end

local function StopTimerForSpell(spellID)
    local active = activeTimers[spellID]
    if not active then return end
    if active.ticker then active.ticker:Cancel() end
    active.frame:Hide()
    active.frame:SetAlpha(0)
    activeTimers[spellID] = nil
    RelayoutTimers()
end

local function ShowSkillTimer(spellID)
    if timerMoveMode then return end
    local entry = timerMap[tonumber(spellID)] or timerMap[spellID]
    if not entry or entry.duration <= 0 then return end

    StopTimerForSpell(spellID)

    local f = GetPooledTimerFrame()
    local t = f.timerText

    local font = GameFontNormal:GetFont()
    t:SetFont(font, entry.fontSz or 22, "OUTLINE")
    t:SetTextColor(entry.r or 1, entry.g or 1, entry.b or 1)

    if entry.bgEnabled then
        f:SetBackdropColor(0,0,0, entry.bgAlpha or 0.5)
        f:SetBackdropBorderColor(0.3,0.3,0.3,0.8)
    else
        f:SetBackdropColor(0,0,0,0)
        f:SetBackdropBorderColor(0,0,0,0)
    end

    local startTime = GetTime()
    local dur       = entry.duration
    local label     = entry.spellName
    local lastCeil  = -1  -- 마지막으로 표시한 초 값 (변경 시에만 SetText·SetSize)

    local function updateText()
        local remaining = math.max(0, dur - (GetTime() - startTime))
        local ceilVal   = math.ceil(remaining)

        -- 표시 값이 바뀌었을 때만 SetText·SetSize 호출 (매 틱 불필요한 렌더링 방지)
        if ceilVal ~= lastCeil then
            lastCeil = ceilVal
            local prefix = ""
            if activeTimers[spellID] and activeTimers[spellID].isAnchor then
                prefix = "★ "
            end
            t:SetText(string.format("%s%s - %d초", prefix, label, ceilVal))
            local tw = t:GetStringWidth() + 20
            local th = t:GetStringHeight() + 14
            f:SetSize(math.max(tw, 80), math.max(th, 30))
        end

        if remaining <= 0 then
            StopTimerForSpell(spellID)
        end
    end

    updateText()
    f:SetAlpha(1)
    f:Show()
    local ticker = C_Timer.NewTicker(0.5, function() updateText() end)
    activeTimers[spellID] = { frame = f, ticker = ticker }
    RelayoutTimers()
end
local function RebuildTimerMap()
    wipe(timerMap)
    local _, classFile = UnitClass("player")
    if not classFile then return end
    local cls = CLASS_BY_ENGLISH[classFile]
    if not cls then return end
    local function loadTimer(tbl)
        if not tbl then return end
        for spellID, data in pairs(tbl) do
            if data.timerEnabled and (data.timerDuration or 0) > 0 then
                local name = (data.timerSpellName and data.timerSpellName ~= "" and data.timerSpellName)
                             or (GetSpellInfo_Safe and GetSpellInfo_Safe(spellID))
                             or ("Spell "..tostring(spellID))
                timerMap[tonumber(spellID) or spellID] = {
                    duration   = data.timerDuration or 0,
                    spellName  = name or ("Spell "..spellID),
                    fontSz     = data.timerFontSize  or 22,
                    r          = data.timerColorR    or 1,
                    g          = data.timerColorG    or 1,
                    b          = data.timerColorB    or 1,
                    bgEnabled  = data.timerBgEnabled == true,
                    bgAlpha    = data.timerBgAlpha   or 0.5,
                    -- 위치는 전역 앵커에서 RelayoutTimers()가 결정
                }
            end
        end
    end
    loadTimer(MoonlightSkillSoundDB[cls.id])
    loadTimer(MoonlightSkillSoundDB[14])
end

local function RebuildRuntimeMap()
    wipe(instantMap)
    wipe(castMap)
    wipe(channelMap)

    local _, classFile = UnitClass("player")
    if not classFile then return end

    -- ★ 역방향 맵으로 O(1) 직업 조회
    local cls = CLASS_BY_ENGLISH[classFile]
    if not cls then return end

    local function loadEntries(tbl)
        if not tbl then return end
        for spellID, data in pairs(tbl) do
            if data.enabled and data.soundPath and data.soundPath ~= "" then
                local entry = { path = data.soundPath, channel = NormalizeChannel(data.soundChannel) }
                local tt = data.triggerType or "instant"
                if tt == "cast" then
                    castMap[spellID] = entry
                elseif tt == "channel" then
                    channelMap[spellID] = entry
                else
                    instantMap[spellID] = entry
                end
            end
        end
    end

    loadEntries(MoonlightSkillSoundDB[cls.id])
    loadEntries(MoonlightSkillSoundDB[14])  -- 장신구: 모든 직업 공통
    RebuildImageMap()
    RebuildTimerMap()
end

-- 중복 재생 방지 테이블 (spellID_triggerType → 마지막 재생 시각)
local recentPlayed = {}

-- 전체 재생률 제한: 0.5초 슬라이딩 윈도우 내 최대 6회
-- AOE·도트 스킬이 동시에 이벤트를 여러 번 발생시킬 때 버스트 차단
local recentPlayLog   = {}
local PLAY_RATE_LIMIT  = 6
local PLAY_RATE_WINDOW = 0.5

-- recentPlayed 자동 청소: 장기 전투 시 테이블이 무한 누적되는 것을 방지
-- 2초 이상 된 항목은 어차피 중복 판정에 쓰이지 않으므로 제거
local function StartRecentPlayedCleanup()
    C_Timer.NewTicker(60, function()
        local now = GetTime()
        for k, t in pairs(recentPlayed) do
            if (now - t) > 2.0 then
                recentPlayed[k] = nil
            end
        end
        -- 재생 로그도 오래된 것 청소
        local cutoff = now - PLAY_RATE_WINDOW * 2
        local j = 0
        for i = 1, #recentPlayLog do
            if recentPlayLog[i] >= cutoff then
                j = j + 1
                recentPlayLog[j] = recentPlayLog[i]
            end
        end
        for i = j + 1, #recentPlayLog do recentPlayLog[i] = nil end
    end)
end

local function PlaySkillSound(spellID, triggerType)
    local map
    if triggerType == "cast" then
        map = castMap
    elseif triggerType == "channel" then
        map = channelMap
    else
        map = instantMap
    end
    local s = map[spellID]
    if not s then return end

    -- 중복 재생 방지: 동일 spellID + triggerType 조합으로 0.2초 이내 재생 차단
    local key = spellID .. "_" .. triggerType
    local now = GetTime()
    if recentPlayed[key] and (now - recentPlayed[key]) < 0.2 then return end
    recentPlayed[key] = now

    -- 전체 재생률 제한: 0.5초 슬라이딩 윈도우 내 PLAY_RATE_LIMIT 회 초과 시 차단
    -- 오래된 항목을 앞에서부터 제거 (in-place)
    local cutoff = now - PLAY_RATE_WINDOW
    local head = 1
    while head <= #recentPlayLog and recentPlayLog[head] < cutoff do
        head = head + 1
    end
    if head > 1 then
        local j = 0
        for i = head, #recentPlayLog do
            j = j + 1
            recentPlayLog[j] = recentPlayLog[i]
        end
        for i = j + 1, #recentPlayLog do recentPlayLog[i] = nil end
    end
    if #recentPlayLog >= PLAY_RATE_LIMIT then return end
    recentPlayLog[#recentPlayLog + 1] = now

    PlaySoundFile(s.path, s.channel)
end

-------------------------------------------------------
-- 스킬 정보 조회
-------------------------------------------------------
local function GetSpellInfo_Safe(spellID)
    if C_Spell and C_Spell.GetSpellInfo then
        local info = C_Spell.GetSpellInfo(spellID)
        if info and info.name then return info.name, info.iconID end
    end
    if GetSpellInfo then
        local name, _, icon = GetSpellInfo(spellID)
        if name then return name, icon end
    end
    return nil, nil
end

local function HexColor(hex)
    return tonumber(hex:sub(1,2),16)/255,
           tonumber(hex:sub(3,4),16)/255,
           tonumber(hex:sub(5,6),16)/255
end

-------------------------------------------------------
-- UI 레이아웃 상수 (BuildTab 함수들과 공유)
-------------------------------------------------------
local COL_TOP = -46
local COL_H   = 560
local X1, COL1_W = 16,  148
local X2, COL2_W = 172, 192
local X3, COL3_W = 372, 472
local TAB_W = COL3_W / 3
local TAB_H = 24
local TAB_Y = -50
local PL = 8  -- 탭 내부 패딩
local PW = COL3_W  -- 탭 패널 너비

-------------------------------------------------------
-- UI 상태 변수
-------------------------------------------------------
local mainFrame         = nil
local selectedClassID   = nil
local selectedSpellID   = nil
local selectedChannel   = "Master"
local selectedTriggerType = "instant"
local classRows         = {}
local skillRows         = {}
local skillScrollContent = nil

local detailSpellIcon, detailSpellName
local detailPathEdit
local detailSMPickerBtn, detailSMLabel, detailSMHint
local smPickerPanel = nil
local detailChannelDropdown
local detailTriggerDropdown
local detailEnabledCheck
local detailSaveBtn, detailTestBtn, detailDeleteBtn
local detailStatusFS

-- 이미지 플래시 UI 변수
local detailImageEnabledCheck
local detailImagePathEdit
local detailImageDurationMinus, detailImageDurationPlus, detailImageDurationDisplay
local detailImageScaleDisplay
local detailImageMoveBtn
-- 페이드 효과 UI
local detailImageFadeCheck
local detailImageFadeInDisplay, detailImageFadeOutDisplay

-- imgMoveMode는 상단 이미지 블록에서 이미 선언됨

-- 초시계 UI 변수
local detailTimerEnabledCheck
local detailTimerDurationEdit
local detailTimerFontSizeDisplay
local detailTimerColorR, detailTimerColorG, detailTimerColorB  -- 현재 선택색 (숫자)
local detailTimerColorBtn
local detailTimerBgCheck
local detailTimerBgAlphaDisplay
local detailTimerMoveBtn

-------------------------------------------------------
-- 헬퍼
-------------------------------------------------------
local function SetStatus(text, r, g, b)
    if detailStatusFS then
        detailStatusFS:SetText(text or "")
        detailStatusFS:SetTextColor(r or 1, g or 0.82, b or 0)
    end
end

local function SetChannel(ch)
    selectedChannel = NormalizeChannel(ch) -- ★ 항상 정규화
    if detailChannelDropdown then
        UIDropDownMenu_SetText(detailChannelDropdown, selectedChannel)
    end
end

local TRIGGER_LABELS = {
    instant = "즉시 시전 (Instant)",
    cast    = "캐스팅 (Cast)",
    channel = "채널링 (Channel)",
}
local function SetTriggerType(tt)
    selectedTriggerType = tt or "instant"
    if detailTriggerDropdown then
        UIDropDownMenu_SetText(detailTriggerDropdown, TRIGGER_LABELS[selectedTriggerType] or selectedTriggerType)
    end
end

-------------------------------------------------------
-- ★ 프리셋 초기화 공통 함수 (resetClassBtn, resetAllBtn 중복 제거)
-------------------------------------------------------
local function ResetClassToPreset(classID)
    if not classID then return false end
    local source = MoonlightSkillSoundDefaults and MoonlightSkillSoundDefaults[classID]
    if not source then return false end
    MoonlightSkillSoundDB[classID] = {}
    for spellID, data in pairs(source) do
        MoonlightSkillSoundDB[classID][spellID] = {
            soundPath      = data.soundPath,
            soundChannel   = NormalizeChannel(data.soundChannel),
            enabled        = (data.enabled ~= false),
            triggerType    = data.triggerType or "instant",
            -- 이미지 필드
            imagePath        = data.imagePath        or "",
            imageEnabled     = data.imageEnabled     == true,
            imageDuration    = data.imageDuration    or 3.0,
            imageScale       = data.imageScale       or 1.0,
            imagePosX        = data.imagePosX        or 0,
            imagePosY        = data.imagePosY        or 0,
            imagePosAnchor   = data.imagePosAnchor   or nil,
            imageFadeEnabled = data.imageFadeEnabled == true,
            imageFadeIn      = data.imageFadeIn      or 0.3,
            imageFadeOut     = data.imageFadeOut     or 0.4,

            -- 초시계 필드
            timerEnabled   = data.timerEnabled   == true,
            timerDuration  = data.timerDuration  or 0,
            timerSpellName = data.timerSpellName or "",
            timerFontSize  = data.timerFontSize  or 22,
            timerColorR    = data.timerColorR    or 1,
            timerColorG    = data.timerColorG    or 1,
            timerColorB    = data.timerColorB    or 1,
            timerBgEnabled = data.timerBgEnabled == true,
            timerBgAlpha   = data.timerBgAlpha   or 0.5,
            timerPosX      = data.timerPosX      or 0,
            timerPosY      = data.timerPosY      or 0,
            timerPosAnchor = data.timerPosAnchor or nil,
        }
    end
    return true
end

-- 현재 선택된 스킬 하나만 프리셋으로 초기화
local function ResetSpellToPreset(classID, spellID)
    if not classID or not spellID then return false end
    local source = MoonlightSkillSoundDefaults
        and MoonlightSkillSoundDefaults[classID]
        and MoonlightSkillSoundDefaults[classID][spellID]
    if not source then return false end
    if not MoonlightSkillSoundDB[classID] then MoonlightSkillSoundDB[classID] = {} end
    MoonlightSkillSoundDB[classID][spellID] = {
        soundPath      = source.soundPath,
        soundChannel   = NormalizeChannel(source.soundChannel),
        enabled        = (source.enabled ~= false),
        triggerType    = source.triggerType or "instant",
        imagePath        = source.imagePath        or "",
        imageEnabled     = source.imageEnabled     == true,
        imageDuration    = source.imageDuration    or 3.0,
        imageScale       = source.imageScale       or 1.0,
        imagePosX        = source.imagePosX        or 0,
        imagePosY        = source.imagePosY        or 0,
        imagePosAnchor   = source.imagePosAnchor   or nil,
        imageFadeEnabled = source.imageFadeEnabled == true,
        imageFadeIn      = source.imageFadeIn      or 0.3,
        imageFadeOut     = source.imageFadeOut     or 0.4,

        timerEnabled   = source.timerEnabled   == true,
        timerDuration  = source.timerDuration  or 0,
        timerSpellName = source.timerSpellName or "",
        timerFontSize  = source.timerFontSize  or 22,
        timerColorR    = source.timerColorR    or 1,
        timerColorG    = source.timerColorG    or 1,
        timerColorB    = source.timerColorB    or 1,
        timerBgEnabled = source.timerBgEnabled == true,
        timerBgAlpha   = source.timerBgAlpha   or 0.5,
        timerPosX      = source.timerPosX      or 0,
        timerPosY      = source.timerPosY      or 0,
        timerPosAnchor = source.timerPosAnchor or nil,
    }
    return true
end

-------------------------------------------------------
-- 스킬 컬럼 갱신
-------------------------------------------------------
local function RefreshSkillColumn(classID)
    for _, row in ipairs(skillRows) do row:Hide() end
    if not classID or not skillScrollContent then return end

    local db  = MoonlightSkillSoundDB[classID] or {}
    local ids = {}
    for sid in pairs(db) do table.insert(ids, sid) end

    -- ★ 정렬 전 이름 미리 캐싱: 정렬 중 API 중복 호출 방지
    local nameCache = {}
    for _, sid in ipairs(ids) do
        nameCache[sid] = GetSpellInfo_Safe(sid) or ("Spell " .. sid)
    end
    table.sort(ids, function(a, b) return nameCache[a] < nameCache[b] end)

    for i, spellID in ipairs(ids) do
        local row = skillRows[i]
        if not row then
            row = CreateFrame("Button", nil, skillScrollContent)
            row:SetSize(185, 30)

            row.bg = row:CreateTexture(nil, "BACKGROUND")
            row.bg:SetAllPoints()
            row.bg:SetColorTexture(1,1,1,0.04)

            row.selBg = row:CreateTexture(nil, "ARTWORK")
            row.selBg:SetAllPoints()
            row.selBg:SetColorTexture(1,0.82,0,0.14)
            row.selBg:Hide()

            row.icon = row:CreateTexture(nil, "ARTWORK")
            row.icon:SetSize(20, 20)
            row.icon:SetPoint("LEFT", 6, 0)
            row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

            row.lbl = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            row.lbl:SetPoint("LEFT", row.icon, "RIGHT", 5, 0)
            row.lbl:SetWidth(128)
            row.lbl:SetJustifyH("LEFT")

            row.dot = row:CreateTexture(nil, "OVERLAY")
            row.dot:SetSize(7, 7)
            row.dot:SetPoint("RIGHT", -5, 0)

            row:SetScript("OnClick", function(self)
                selectedSpellID = self.spellID
                for _, r in ipairs(skillRows) do
                    if r:IsShown() then
                        if r.spellID == self.spellID then r.selBg:Show() else r.selBg:Hide() end
                    end
                end
                LoadDetailPanel(self.classID, self.spellID)
            end)
            skillRows[i] = row
        end

        row:ClearAllPoints()
        if i == 1 then
            row:SetPoint("TOPLEFT", skillScrollContent, "TOPLEFT", 0, 0)
        else
            row:SetPoint("TOPLEFT", skillRows[i-1], "BOTTOMLEFT", 0, -2)
        end

        -- ★ 캐시된 이름 재사용
        local name = nameCache[spellID]
        local _, icon = GetSpellInfo_Safe(spellID)
        row.icon:SetTexture(icon or 134400)
        row.lbl:SetText(name .. " |cff888888("..spellID..")|r")
        row.spellID = spellID
        row.classID = classID

        local entry = db[spellID]
        local hasSound  = entry and entry.soundPath and entry.soundPath ~= ""
        local isEnabled = entry and entry.enabled ~= false
        if hasSound and isEnabled then
            row.dot:SetColorTexture(0.2, 1, 0.2, 1); row.dot:Show()
        elseif hasSound then
            row.dot:SetColorTexture(1, 0.6, 0, 1); row.dot:Show()
        else
            row.dot:Hide()
        end

        if selectedSpellID == spellID and selectedClassID == classID then
            row.selBg:Show()
        else
            row.selBg:Hide()
        end
        row:Show()
    end

    skillScrollContent:SetHeight(math.max(1, #ids * 32))
end

-------------------------------------------------------
-- Detail 패널 채우기
-------------------------------------------------------
function LoadDetailPanel(classID, spellID)
    selectedClassID = classID
    selectedSpellID = spellID

    if not spellID then
        if detailSpellIcon    then detailSpellIcon:SetTexture(134400) end
        if detailSpellName    then detailSpellName:SetText("스킬을 선택하세요") end
        if detailPathEdit     then detailPathEdit:SetText("") end
        if detailEnabledCheck then detailEnabledCheck:SetChecked(false) end
        SetChannel("Master")
        SetTriggerType("instant")
        if detailSaveBtn   then detailSaveBtn:Disable()   end
        if detailTestBtn   then detailTestBtn:Disable()   end -- ★ 미선택 시 비활성화
        if detailDeleteBtn then detailDeleteBtn:Disable() end
        SetStatus("")
        return
    end

    local name, icon = GetSpellInfo_Safe(spellID)
    if detailSpellIcon then detailSpellIcon:SetTexture(icon or 134400) end
    if detailSpellName then detailSpellName:SetText(name or ("Spell "..spellID)) end

    local entry = MoonlightSkillSoundDB[classID] and MoonlightSkillSoundDB[classID][spellID]
    if entry then
        if detailPathEdit     then detailPathEdit:SetText(entry.soundPath or "") end
        if detailEnabledCheck then detailEnabledCheck:SetChecked(entry.enabled ~= false) end
        SetChannel(entry.soundChannel or "Master")
        SetTriggerType(entry.triggerType or "instant")
        -- 이미지 필드 복원
        if detailImageEnabledCheck then detailImageEnabledCheck:SetChecked(entry.imageEnabled == true) end
        if detailImagePathEdit     then detailImagePathEdit:SetText(entry.imagePath or "") end
        if detailImageDurationDisplay then
            detailImageDurationDisplay:SetText(string.format("%.1f", entry.imageDuration or 3.0))
        end
        if detailImageScaleDisplay then
            detailImageScaleDisplay:SetText(string.format("%.2f", entry.imageScale or 1.0))
        end
        -- 페이드 효과
        if detailImageFadeCheck      then detailImageFadeCheck:SetChecked(entry.imageFadeEnabled == true) end
        if detailImageFadeInDisplay  then detailImageFadeInDisplay:SetText(string.format("%.2f", entry.imageFadeIn  or 0.3)) end
        if detailImageFadeOutDisplay then detailImageFadeOutDisplay:SetText(string.format("%.2f", entry.imageFadeOut or 0.4)) end
        -- 줌인 효과

        -- 초시계 필드 복원
        if detailTimerEnabledCheck   then detailTimerEnabledCheck:SetChecked(entry.timerEnabled == true) end
        if detailTimerDurationEdit   then detailTimerDurationEdit:SetText(tostring(entry.timerDuration or 0)) end
        if detailTimerFontSizeDisplay then detailTimerFontSizeDisplay:SetText(tostring(entry.timerFontSize or 22)) end
        detailTimerColorR = entry.timerColorR or 1
        detailTimerColorG = entry.timerColorG or 1
        detailTimerColorB = entry.timerColorB or 1
        if detailTimerBgCheck        then detailTimerBgCheck:SetChecked(entry.timerBgEnabled == true) end
        if detailTimerBgAlphaDisplay then detailTimerBgAlphaDisplay:SetText(string.format("%.1f", entry.timerBgAlpha or 0.5)) end
    else
        if detailPathEdit     then detailPathEdit:SetText("") end
        if detailEnabledCheck then detailEnabledCheck:SetChecked(true) end
        SetChannel("Master")
        SetTriggerType("instant")
        if detailImageEnabledCheck    then detailImageEnabledCheck:SetChecked(false) end
        if detailImagePathEdit        then detailImagePathEdit:SetText("") end
        if detailImageDurationDisplay then detailImageDurationDisplay:SetText("3.0") end
        if detailImageScaleDisplay    then detailImageScaleDisplay:SetText("1.00") end
        if detailImageFadeCheck       then detailImageFadeCheck:SetChecked(false) end
        if detailImageFadeInDisplay   then detailImageFadeInDisplay:SetText("0.30") end
        if detailImageFadeOutDisplay  then detailImageFadeOutDisplay:SetText("0.40") end

        if detailTimerEnabledCheck    then detailTimerEnabledCheck:SetChecked(false) end
        if detailTimerDurationEdit    then detailTimerDurationEdit:SetText("0") end
        if detailTimerFontSizeDisplay then detailTimerFontSizeDisplay:SetText("22") end
        detailTimerColorR = 1; detailTimerColorG = 1; detailTimerColorB = 1
        if detailTimerBgCheck         then detailTimerBgCheck:SetChecked(false) end
        if detailTimerBgAlphaDisplay  then detailTimerBgAlphaDisplay:SetText("0.5") end
    end

    if detailSaveBtn   then detailSaveBtn:Enable()   end
    if detailTestBtn   then detailTestBtn:Enable()   end -- ★ 선택 시 활성화
    if detailDeleteBtn then detailDeleteBtn:Enable() end
    if detailSMPickerBtn then detailSMPickerBtn:SetText("선택...") end
    SetStatus("")
end

-------------------------------------------------------
-- 직업 행 클릭
-------------------------------------------------------
local function OnClassRowClick(self)
    selectedClassID = self.classID
    selectedSpellID = nil
    for _, r in ipairs(classRows) do
        if r.classID == self.classID then
            r.bg:SetColorTexture(1, 0.82, 0, 0.15)
        else
            r.bg:SetColorTexture(1, 1, 1, 0.04)
        end
    end
    RefreshSkillColumn(self.classID)
    LoadDetailPanel(self.classID, nil)
end

-------------------------------------------------------
-- UI 공통 헬퍼 (모듈 레벨 — upvalue 절약)
-------------------------------------------------------
local function UI_MakeBox(parent, px, py, pw, ph)
    local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    f:SetPoint("TOPLEFT", parent, "TOPLEFT", px, py)
    f:SetSize(pw, ph)
    f:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
    })
    f:SetBackdropColor(0, 0, 0, 0.22)
    f:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.8)
    return f
end

local function UI_MakeDivider(parent, yOff)
    local d = parent:CreateTexture(nil, "ARTWORK")
    d:SetColorTexture(0.3, 0.3, 0.3, 0.7)
    d:SetPoint("TOPLEFT",  parent, "TOPLEFT",  0, yOff)
    d:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, yOff)
    d:SetHeight(1)
    return d
end

local function UI_MakeDropdown(parent, x, y, w, h, items, getVal, setVal, displayFunc)
    local btn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btn:SetSize(w, h)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    local bTxt = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    bTxt:SetPoint("LEFT",  btn, "LEFT",  8, 0)
    bTxt:SetPoint("RIGHT", btn, "RIGHT", -18, 0)
    bTxt:SetJustifyH("LEFT")
    bTxt:SetText(displayFunc and displayFunc(getVal()) or getVal())
    btn.label = bTxt
    local arrow = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    arrow:SetPoint("RIGHT", btn, "RIGHT", -6, 0)
    arrow:SetText("v")
    local popup = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    popup:SetSize(w, #items * h)
    popup:SetFrameStrata("TOOLTIP"); popup:SetFrameLevel(200)
    popup:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
    })
    popup:SetBackdropBorderColor(0.5, 0.45, 0.1, 1)
    popup:Hide()
    local itemBtns = {}
    for i, item in ipairs(items) do
        local ib = CreateFrame("Button", nil, popup)
        ib:SetSize(w, h)
        ib:SetPoint("TOPLEFT", popup, "TOPLEFT", 0, -(i-1)*h)
        local ibBg = ib:CreateTexture(nil, "BACKGROUND")
        ibBg:SetAllPoints(); ibBg:SetColorTexture(1,1,1,0)
        ib.bg = ibBg
        local ibTxt = ib:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        ibTxt:SetPoint("LEFT", ib, "LEFT", 8, 0); ibTxt:SetJustifyH("LEFT")
        ibTxt:SetText(item.label or item.value)
        ib:SetScript("OnEnter", function() ibBg:SetColorTexture(1,0.82,0,0.2) end)
        ib:SetScript("OnLeave", function()
            if getVal() == item.value then ibBg:SetColorTexture(1,0.82,0,0.12)
            else ibBg:SetColorTexture(1,1,1,0) end
        end)
        ib:SetScript("OnClick", function()
            setVal(item.value)
            bTxt:SetText(displayFunc and displayFunc(item.value) or item.value)
            for j = 1, #items do
                if getVal() == items[j].value then itemBtns[j].bg:SetColorTexture(1,0.82,0,0.12)
                else itemBtns[j].bg:SetColorTexture(1,1,1,0) end
            end
            popup:Hide()
        end)
        itemBtns[i] = ib
    end
    btn:SetScript("OnClick", function()
        if popup:IsShown() then popup:Hide()
        else
            for i = 1, #items do
                if getVal() == items[i].value then itemBtns[i].bg:SetColorTexture(1,0.82,0,0.12)
                else itemBtns[i].bg:SetColorTexture(1,1,1,0) end
            end
            popup:ClearAllPoints()
            popup:SetPoint("TOPLEFT", btn, "BOTTOMLEFT", 0, -2)
            popup:Show()
        end
    end)
    btn.Refresh = function()
        bTxt:SetText(displayFunc and displayFunc(getVal()) or getVal())
    end
    return btn
end

-- SaveAllFields / AutoSave (모듈 레벨 — BuildTab들이 참조 가능)
local AutoSave  -- forward declare
local function SaveAllFields(silent)
    if not selectedClassID or not selectedSpellID then return end
    local db = MoonlightSkillSoundDB[selectedClassID]
    if not db[selectedSpellID] then db[selectedSpellID] = {} end
    local e = db[selectedSpellID]
    if detailPathEdit     then e.soundPath    = detailPathEdit:GetText() or "" end
    e.soundChannel = selectedChannel
    if detailEnabledCheck then e.enabled      = detailEnabledCheck:GetChecked() end
    e.triggerType  = selectedTriggerType
    if detailImageEnabledCheck  then e.imageEnabled  = detailImageEnabledCheck:GetChecked() end
    if detailImagePathEdit      then e.imagePath     = detailImagePathEdit:GetText() or "" end
    if detailImageDurationDisplay then e.imageDuration = tonumber(detailImageDurationDisplay:GetText()) or 3.0 end
    if detailImageScaleDisplay  then
        local sc = tonumber(detailImageScaleDisplay:GetText()) or 1.0
        e.imageScale = sc; imgFrame:SetScale(sc)
    end
    -- 페이드 효과
    if detailImageFadeCheck      then e.imageFadeEnabled = detailImageFadeCheck:GetChecked() end
    if detailImageFadeInDisplay  then e.imageFadeIn      = tonumber(detailImageFadeInDisplay:GetText())  or 0.3 end
    if detailImageFadeOutDisplay then e.imageFadeOut     = tonumber(detailImageFadeOutDisplay:GetText()) or 0.4 end

    -- 이미지 위치 (OnDragStop에서 직접 저장되지만 SaveAllFields에서도 보존)
    -- DB에 이미 있는 값을 그대로 유지 (OnDragStop이 먼저 썼을 경우 덮어쓰지 않음)
    if e.imagePosX == nil then e.imagePosX = 0 end
    if e.imagePosY == nil then e.imagePosY = 0 end
    if detailTimerEnabledCheck    then e.timerEnabled   = detailTimerEnabledCheck:GetChecked() end
    if detailTimerDurationEdit    then e.timerDuration  = tonumber(detailTimerDurationEdit:GetText()) or 0 end
    local spellName = GetSpellInfo_Safe(selectedSpellID) or ("Spell "..tostring(selectedSpellID))
    e.timerSpellName = spellName
    if detailTimerFontSizeDisplay then e.timerFontSize  = tonumber(detailTimerFontSizeDisplay:GetText()) or 22 end
    e.timerColorR = detailTimerColorR or 1
    e.timerColorG = detailTimerColorG or 1
    e.timerColorB = detailTimerColorB or 1
    if detailTimerBgCheck        then e.timerBgEnabled = detailTimerBgCheck:GetChecked() end
    if detailTimerBgAlphaDisplay then e.timerBgAlpha   = tonumber(detailTimerBgAlphaDisplay:GetText()) or 0.5 end
    RebuildRuntimeMap()
    RefreshSkillColumn(selectedClassID)
    if not silent then SetStatus("저장되었습니다!", 0.3,1,0.3) end
end
AutoSave = function() SaveAllFields(true) end


-- ════════════════════════════════════════════════
local function BuildTab1(tabPanels)
local P  = tabPanels[1]

local pathLbl = P:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
pathLbl:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -8)
pathLbl:SetText("|cffaaaaaa사운드 파일 경로 (직접 입력)|r")

detailPathEdit = CreateFrame("EditBox", nil, P, "InputBoxTemplate")
detailPathEdit:SetSize(PW - PL*2, 20)
detailPathEdit:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -24)
detailPathEdit:SetAutoFocus(false); detailPathEdit:SetMaxLetters(512)

detailSMLabel = P:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
detailSMLabel:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -52)
detailSMLabel:SetTextColor(0.4,0.8,1)
detailSMLabel:SetText("또는 SharedMedia에서 선택:")

detailSMHint = P:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
detailSMHint:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -52)
detailSMHint:SetTextColor(0.4,0.4,0.4)
detailSMHint:SetText("SharedMedia가 설치되어 있지 않습니다.")
detailSMHint:Hide()

detailSMPickerBtn = CreateFrame("Button", nil, P, "UIPanelButtonTemplate")
detailSMPickerBtn:SetSize(PW - PL*2, 22)
detailSMPickerBtn:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -66)
detailSMPickerBtn:SetText("선택…")

local function BuildSMPanel()
    if smPickerPanel then return end
    smPickerPanel = CreateFrame("Frame", "MSSPickerPanel", UIParent, "BackdropTemplate")
    smPickerPanel:SetSize(300, 360)
    smPickerPanel:SetFrameStrata("TOOLTIP"); smPickerPanel:SetFrameLevel(100)
    smPickerPanel:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        tile = false, edgeSize = 1,
    })
    smPickerPanel:SetBackdropBorderColor(0.4,0.8,1,0.9)
    smPickerPanel:Hide()
    table.insert(UISpecialFrames, "MSSPickerPanel")

    local searchBox = CreateFrame("EditBox", nil, smPickerPanel, "InputBoxTemplate")
    searchBox:SetSize(260, 22)
    searchBox:SetPoint("TOPLEFT", smPickerPanel, "TOPLEFT", 10, -10)
    searchBox:SetAutoFocus(false); searchBox:SetMaxLetters(64)

    local searchHint = smPickerPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    searchHint:SetPoint("LEFT", searchBox, "LEFT", 4, 0)
    searchHint:SetTextColor(0.5,0.5,0.5); searchHint:SetText("검색…")
    searchBox:SetScript("OnTextChanged", function(self)
        searchHint:SetShown(self:GetText() == "")
        smPickerPanel.filterText = self:GetText():lower()
        smPickerPanel.Populate()
    end)

    local scroll = CreateFrame("ScrollFrame", nil, smPickerPanel, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT",     smPickerPanel, "TOPLEFT",      4, -38)
    scroll:SetPoint("BOTTOMRIGHT", smPickerPanel, "BOTTOMRIGHT", -24,  4)

    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(272, 1); scroll:SetScrollChild(content)

    smPickerPanel:EnableMouseWheel(true)
    smPickerPanel:SetScript("OnMouseWheel", function(_, delta)
        local cur = scroll:GetVerticalScroll()
        local maxScroll = math.max(0, content:GetHeight() - scroll:GetHeight())
        scroll:SetVerticalScroll(math.max(0, math.min(maxScroll, cur - delta*20)))
    end)

    local smRows = {}
    local ROW_H  = 22

    smPickerPanel.Populate = function()
        local LSM    = LibStub and LibStub("LibSharedMedia-3.0", true)
        local sounds = LSM and LSM:List("sound") or {}
        local filter = smPickerPanel.filterText or ""
        local filtered = {}
        for _, name in ipairs(sounds) do
            if filter == "" or name:lower():find(filter, 1, true) then
                filtered[#filtered+1] = name
            end
        end
        for _, r in ipairs(smRows) do r:Hide() end
        for i, name in ipairs(filtered) do
            local row = smRows[i]
            if not row then
                row = CreateFrame("Button", nil, content)
                row:SetSize(272, ROW_H)
                row.bg = row:CreateTexture(nil, "BACKGROUND")
                row.bg:SetAllPoints(); row.bg:SetColorTexture(1,1,1,0.04)
                row.lbl = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
                row.lbl:SetPoint("LEFT", 6, 0); row.lbl:SetWidth(220); row.lbl:SetJustifyH("LEFT")
                row.playBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
                row.playBtn:SetSize(36, 18); row.playBtn:SetPoint("RIGHT", row, "RIGHT", -2, 0)
                row.playBtn:SetText("▶")
                row:SetScript("OnEnter", function(s) s.bg:SetColorTexture(1,0.82,0,0.12) end)
                row:SetScript("OnLeave", function(s) s.bg:SetColorTexture(1,1,1,0.04) end)
                smRows[i] = row
            end
            local capName = name
            row.lbl:SetText(capName)
            row:SetScript("OnClick", function()
                local LSM2 = LibStub and LibStub("LibSharedMedia-3.0", true)
                local path = LSM2 and LSM2:Fetch("sound", capName)
                if path then detailPathEdit:SetText(path); detailSMPickerBtn:SetText(capName) end
                smPickerPanel:Hide()
            end)
            row.playBtn:SetScript("OnClick", function()
                local LSM2 = LibStub and LibStub("LibSharedMedia-3.0", true)
                local path = LSM2 and LSM2:Fetch("sound", capName)
                if path then PlaySoundFile(path, "Master") end
            end)
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(i-1)*ROW_H)
            row:Show()
        end
        content:SetHeight(math.max(1, #filtered*ROW_H))
        scroll:SetVerticalScroll(0)
    end
    smPickerPanel.searchBox = searchBox
end

detailSMPickerBtn:SetScript("OnClick", function(self)
    BuildSMPanel()
    if smPickerPanel:IsShown() then smPickerPanel:Hide()
    else
        smPickerPanel.filterText = ""
        smPickerPanel.searchBox:SetText("")
        smPickerPanel.Populate()
        smPickerPanel:ClearAllPoints()
        smPickerPanel:SetPoint("BOTTOMLEFT", self, "TOPLEFT", 0, 4)
        smPickerPanel:Show()
    end
end)

local function UpdateSMVisibility()
    local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM then detailSMLabel:Show(); detailSMPickerBtn:Show(); detailSMHint:Hide()
    else detailSMLabel:Hide(); detailSMPickerBtn:Hide(); detailSMHint:Show() end
end
UpdateSMVisibility()

UI_MakeDivider(P, -96)

-- 채널 / 트리거 (2열 커스텀 드롭다운)
local HW = math.floor((PW - PL*2 - 8) / 2)

local chanLbl = P:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
chanLbl:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -104)
chanLbl:SetText("|cffaaaaaa채널|r")

local trigLbl = P:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
trigLbl:SetPoint("TOPLEFT", P, "TOPLEFT", PL+HW+8, -104)
trigLbl:SetText("|cffaaaaaa시전 타입|r")

local chanItems = {}
for _, ch in ipairs(CHANNELS) do chanItems[#chanItems+1] = { label=ch, value=ch } end

local trigItems = {
    { label="즉시 (Instant)", value="instant" },
    { label="캐스팅 (Cast)",  value="cast"    },
    { label="채널링 (Chan.)", value="channel" },
}
local function GetTrigLabel(v)
    for _, t in ipairs(trigItems) do if t.value == v then return t.label end end
    return v
end

detailChannelDropdown = UI_MakeDropdown(P, PL, -118, HW, 22,
    chanItems,
    function() return selectedChannel end,
    function(v) SetChannel(v) end)

detailTriggerDropdown = UI_MakeDropdown(P, PL+HW+8, -118, HW, 22,
    trigItems,
    function() return selectedTriggerType end,
    function(v) SetTriggerType(v) end,
    GetTrigLabel)

-- SetChannel / SetTriggerType 오버라이드 (커스텀 드롭다운 갱신)
SetChannel = function(ch)
    selectedChannel = NormalizeChannel(ch)
    if detailChannelDropdown and detailChannelDropdown.Refresh then
        detailChannelDropdown.Refresh()
    end
end
SetTriggerType = function(tt)
    selectedTriggerType = tt or "instant"
    if detailTriggerDropdown and detailTriggerDropdown.Refresh then
        detailTriggerDropdown.Refresh()
    end
end

UI_MakeDivider(P, -148)

-- 활성화 체크
detailEnabledCheck = CreateFrame("CheckButton", "MSSEnabledCheck", P, "UICheckButtonTemplate")
detailEnabledCheck:SetSize(22, 22)
detailEnabledCheck:SetPoint("TOPLEFT", P, "TOPLEFT", PL-2, -154)
detailEnabledCheck:SetChecked(true)

local enableLbl = P:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
enableLbl:SetPoint("LEFT", detailEnabledCheck, "RIGHT", 2, 0)
enableLbl:SetText("이 스킬 사운드 활성화")

UI_MakeDivider(P, -184)

-- 저장 / 테스트 / 삭제
local BW = math.floor((PW - PL*2 - 8) / 3)

detailSaveBtn = CreateFrame("Button", nil, P, "UIPanelButtonTemplate")
detailSaveBtn:SetSize(BW, 26)
detailSaveBtn:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -192)
detailSaveBtn:SetText("저장"); detailSaveBtn:Disable()

detailTestBtn = CreateFrame("Button", nil, P, "UIPanelButtonTemplate")
detailTestBtn:SetSize(BW, 26)
detailTestBtn:SetPoint("LEFT", detailSaveBtn, "RIGHT", 4, 0)
detailTestBtn:SetText("▶ 테스트"); detailTestBtn:Disable()

detailDeleteBtn = CreateFrame("Button", nil, P, "UIPanelButtonTemplate")
detailDeleteBtn:SetSize(BW, 26)
detailDeleteBtn:SetPoint("LEFT", detailTestBtn, "RIGHT", 4, 0)
detailDeleteBtn:SetText("삭제"); detailDeleteBtn:Disable()

detailStatusFS = P:CreateFontString(nil, "OVERLAY", "GameFontNormal")
detailStatusFS:SetPoint("TOPLEFT", P, "TOPLEFT", PL, -228)
detailStatusFS:SetWidth(PW - PL*2)
detailStatusFS:SetJustifyH("LEFT")
detailStatusFS:SetText("")

-- ════════════════════════════════════════════════
--  탭2: 이미지 플래시
-- ════════════════════════════════════════════════
end

-- ════════════════════════════════════════════════
local function BuildTab2(tabPanels)
local P2 = tabPanels[2]

detailImageEnabledCheck = CreateFrame("CheckButton", nil, P2, "UICheckButtonTemplate")
detailImageEnabledCheck:SetSize(22, 22)
detailImageEnabledCheck:SetPoint("TOPLEFT", P2, "TOPLEFT", PL-2, -6)
detailImageEnabledCheck:SetChecked(false)
local imgEnableLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
imgEnableLbl:SetPoint("LEFT", detailImageEnabledCheck, "RIGHT", 2, 0)
imgEnableLbl:SetText("이미지 표시 활성화")

UI_MakeDivider(P2, -36)

local imgPathLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
imgPathLbl:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -44)
imgPathLbl:SetText("|cffaaaaaa이미지 경로  (.tga / .png)|r")

detailImagePathEdit = CreateFrame("EditBox", nil, P2, "InputBoxTemplate")
detailImagePathEdit:SetSize(PW - PL*2, 20)
detailImagePathEdit:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -60)
detailImagePathEdit:SetAutoFocus(false); detailImagePathEdit:SetMaxLetters(512)

UI_MakeDivider(P2, -90)

local imgHW = math.floor((PW - PL*2 - 8) / 2)

local imgDurLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
imgDurLbl:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -98)
imgDurLbl:SetText("|cffaaaaaa표시 시간 (초)|r")

local imgScaleLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
imgScaleLbl:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+imgHW+8, -98)
imgScaleLbl:SetText("|cffaaaaaa배율 (직접 입력 가능)|r")

detailImageDurationMinus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
detailImageDurationMinus:SetSize(26, 22)
detailImageDurationMinus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -118)
detailImageDurationMinus:SetText("-")
detailImageDurationMinus:SetScript("OnClick", function()
    local cur = tonumber(detailImageDurationDisplay:GetText()) or 3.0
    detailImageDurationDisplay:SetText(string.format("%.1f", math.max(0.5, cur-0.5)))
end)

detailImageDurationDisplay = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailImageDurationDisplay:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+28, -118)
detailImageDurationDisplay:SetWidth(imgHW - 56)
detailImageDurationDisplay:SetJustifyH("CENTER")
detailImageDurationDisplay:SetText("3.0")

detailImageDurationPlus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
detailImageDurationPlus:SetSize(26, 22)
detailImageDurationPlus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+imgHW-26, -118)
detailImageDurationPlus:SetText("+")
detailImageDurationPlus:SetScript("OnClick", function()
    local cur = tonumber(detailImageDurationDisplay:GetText()) or 3.0
    detailImageDurationDisplay:SetText(string.format("%.1f", math.min(30.0, cur+0.5)))
end)

local imgScaleMinus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
imgScaleMinus:SetSize(26, 22)
imgScaleMinus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+imgHW+8, -118)
imgScaleMinus:SetText("-")
imgScaleMinus:SetScript("OnClick", function()
    local v = math.max(0.1, math.floor(((tonumber(detailImageScaleDisplay:GetText()) or 1.0)-0.1)*100+0.5)/100)
    detailImageScaleDisplay:SetText(string.format("%.2f", v)); imgFrame:SetScale(v)
end)

detailImageScaleDisplay = CreateFrame("EditBox", nil, P2, "InputBoxTemplate")
detailImageScaleDisplay:SetSize(imgHW-56, 22)
detailImageScaleDisplay:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+imgHW+8+28, -118)
detailImageScaleDisplay:SetAutoFocus(false); detailImageScaleDisplay:SetMaxLetters(6)
detailImageScaleDisplay:SetText("1.00"); detailImageScaleDisplay:SetJustifyH("CENTER")
detailImageScaleDisplay:SetScript("OnEnterPressed", function(self)
    local v = tonumber(self:GetText())
    if v then v=math.max(0.1,math.min(5.0,v)); self:SetText(string.format("%.2f",v)); imgFrame:SetScale(v)
    else self:SetText("1.00"); imgFrame:SetScale(1.0) end
    self:ClearFocus()
end)
detailImageScaleDisplay:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)

local imgScalePlus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
imgScalePlus:SetSize(26, 22)
imgScalePlus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+imgHW+8+imgHW-26, -118)
imgScalePlus:SetText("+")
imgScalePlus:SetScript("OnClick", function()
    local v = math.min(5.0, math.floor(((tonumber(detailImageScaleDisplay:GetText()) or 1.0)+0.1)*100+0.5)/100)
    detailImageScaleDisplay:SetText(string.format("%.2f", v)); imgFrame:SetScale(v)
end)

UI_MakeDivider(P2, -150)

-- 미리보기 + 위치이동 통합 버튼 (3단계)
-- 이미지 미리보기 + 위치이동 통합 버튼 (2단계)
-- 1클릭: 이미지를 화면에 표시하고 드래그로 위치 조정 가능
-- 2클릭: 위치 저장 후 이미지 숨김, 기본 상태로 복귀
local imgPreviewing = false

detailImageMoveBtn = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
detailImageMoveBtn:SetSize(PW - PL*2, 26)
detailImageMoveBtn:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -160)
detailImageMoveBtn:SetText("▶  이미지 미리보기 및 위치이동")
detailImageMoveBtn:GetFontString():SetTextColor(0.3,1,0.7)

local function ImgBtn_SetNormal()
    imgPreviewing = false
    imgMoveMode   = false
    detailImageMoveBtn:SetText("▶  이미지 미리보기 및 위치이동")
    detailImageMoveBtn:GetFontString():SetTextColor(0.3,1,0.7)
end

detailImageMoveBtn:SetScript("OnClick", function()
    if not imgPreviewing then
        -- 미리보기 시작 + 즉시 드래그 가능
        local path = detailImagePathEdit:GetText() or ""
        if path == "" then SetStatus("이미지 경로를 먼저 입력하세요.", 1,0.4,0.4); return end
        imgTexture:SetTexture(path)
        local sc = tonumber(detailImageScaleDisplay:GetText()) or 1.0
        imgFrame:SetSize(256,256); imgFrame:SetScale(sc)
        if imgHideTimer  then imgHideTimer:Cancel();  imgHideTimer  = nil end
        if imgFadeTicker then imgFadeTicker:Cancel(); imgFadeTicker = nil end
        imgTexture:SetAlpha(1); imgFrame:Show()
        imgMoveMode = true
        imgFrame:SetMovable(true); imgFrame:EnableMouse(true)
        imgFrame:RegisterForDrag("LeftButton")
        imgFrame:SetScript("OnDragStart", imgFrame.StartMoving)
        imgFrame:SetScript("OnDragStop", function(self)
            self:StopMovingOrSizing()
            local x = self:GetLeft()
            local y = self:GetBottom()
            -- 앵커를 BOTTOMLEFT로 명시 고정 (ShowSkillImage의 복원 기준과 일치)
            self:ClearAllPoints()
            self:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y)
            if selectedClassID and selectedSpellID then
                local db = MoonlightSkillSoundDB[selectedClassID]
                if db and db[selectedSpellID] then
                    db[selectedSpellID].imagePosX      = x
                    db[selectedSpellID].imagePosY      = y
                    db[selectedSpellID].imagePosAnchor = "BOTTOMLEFT"
                end
            end
            -- imageMap을 즉시 갱신해야 저장 버튼 없이도 위치가 반영됨
            RebuildRuntimeMap()
        end)
        imgPreviewing = true
        detailImageMoveBtn:SetText("|cffff9999■  드래그로 위치 조정 중… (클릭해서 완료)|r")
        detailImageMoveBtn:GetFontString():SetTextColor(1,1,1)
    else
        -- 완료: 위치 저장 후 이미지 숨김
        imgMoveMode = false
        imgFrame:SetMovable(false); imgFrame:EnableMouse(false)
        imgFrame:SetScript("OnDragStart", nil); imgFrame:SetScript("OnDragStop", nil)
        imgFrame:Hide(); imgTexture:SetAlpha(0)
        RebuildRuntimeMap()
        ImgBtn_SetNormal()
        SetStatus("위치가 저장되었습니다. 저장 버튼을 눌러 적용하세요.", 0.3,1,0.7)
    end
end)

UI_MakeDivider(P2, -200)

-- ── 페이드 효과 ──────────────────────────────────────────────
local imgFadeLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
imgFadeLbl:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -208)
imgFadeLbl:SetText("|cffaaaaaa페이드 효과|r")

detailImageFadeCheck = CreateFrame("CheckButton", nil, P2, "UICheckButtonTemplate")
detailImageFadeCheck:SetSize(22, 22)
detailImageFadeCheck:SetPoint("TOPLEFT", P2, "TOPLEFT", PL-2, -222)
detailImageFadeCheck:SetChecked(false)
local imgFadeEnLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
imgFadeEnLbl:SetPoint("LEFT", detailImageFadeCheck, "RIGHT", 2, 0)
imgFadeEnLbl:SetText("페이드 인 / 아웃 활성화")

-- 페이드인 시간
local fadeSW = math.floor((PW - PL*2 - 8) / 2)
local imgFadeInLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
imgFadeInLbl:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -252)
imgFadeInLbl:SetText("|cffaaaaaa페이드 인 (초)|r")

local imgFadeOutLbl = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
imgFadeOutLbl:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+fadeSW+8, -252)
imgFadeOutLbl:SetText("|cffaaaaaa페이드 아웃 (초)|r")

local imgFadeInMinus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
imgFadeInMinus:SetSize(26, 22)
imgFadeInMinus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -268)
imgFadeInMinus:SetText("-")
imgFadeInMinus:SetScript("OnClick", function()
    if not detailImageFadeInDisplay then return end
    local v = math.max(0.05, math.floor(((tonumber(detailImageFadeInDisplay:GetText()) or 0.3) - 0.05)*100+0.5)/100)
    detailImageFadeInDisplay:SetText(string.format("%.2f", v))
end)

detailImageFadeInDisplay = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailImageFadeInDisplay:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+28, -268)
detailImageFadeInDisplay:SetWidth(fadeSW - 56)
detailImageFadeInDisplay:SetJustifyH("CENTER")
detailImageFadeInDisplay:SetText("0.30")

local imgFadeInPlus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
imgFadeInPlus:SetSize(26, 22)
imgFadeInPlus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+fadeSW-26, -268)
imgFadeInPlus:SetText("+")
imgFadeInPlus:SetScript("OnClick", function()
    if not detailImageFadeInDisplay then return end
    local v = math.min(5.0, math.floor(((tonumber(detailImageFadeInDisplay:GetText()) or 0.3) + 0.05)*100+0.5)/100)
    detailImageFadeInDisplay:SetText(string.format("%.2f", v))
end)

local imgFadeOutMinus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
imgFadeOutMinus:SetSize(26, 22)
imgFadeOutMinus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+fadeSW+8, -268)
imgFadeOutMinus:SetText("-")
imgFadeOutMinus:SetScript("OnClick", function()
    if not detailImageFadeOutDisplay then return end
    local v = math.max(0.05, math.floor(((tonumber(detailImageFadeOutDisplay:GetText()) or 0.4) - 0.05)*100+0.5)/100)
    detailImageFadeOutDisplay:SetText(string.format("%.2f", v))
end)

detailImageFadeOutDisplay = P2:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailImageFadeOutDisplay:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+fadeSW+8+28, -268)
detailImageFadeOutDisplay:SetWidth(fadeSW - 56)
detailImageFadeOutDisplay:SetJustifyH("CENTER")
detailImageFadeOutDisplay:SetText("0.40")

local imgFadeOutPlus = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
imgFadeOutPlus:SetSize(26, 22)
imgFadeOutPlus:SetPoint("TOPLEFT", P2, "TOPLEFT", PL+fadeSW+8+fadeSW-26, -268)
imgFadeOutPlus:SetText("+")
imgFadeOutPlus:SetScript("OnClick", function()
    if not detailImageFadeOutDisplay then return end
    local v = math.min(5.0, math.floor(((tonumber(detailImageFadeOutDisplay:GetText()) or 0.4) + 0.05)*100+0.5)/100)
    detailImageFadeOutDisplay:SetText(string.format("%.2f", v))
end)

UI_MakeDivider(P2, -300)

local imgResetBtn = CreateFrame("Button", nil, P2, "UIPanelButtonTemplate")
imgResetBtn:SetSize(PW - PL*2, 22)
imgResetBtn:SetPoint("TOPLEFT", P2, "TOPLEFT", PL, -382)
imgResetBtn:SetText("위치·배율 초기화")
imgResetBtn:SetScript("OnClick", function()
    StaticPopupDialogs["MSS_IMG_RESET_CONFIRM"] = {
        text = "이미지 위치와 배율을 기본값으로 초기화합니다.\n계속하시겠습니까?",
        button1 = "초기화",
        button2 = "취소",
        OnAccept = function()
            if selectedClassID and selectedSpellID then
                local db = MoonlightSkillSoundDB[selectedClassID]
                if db and db[selectedSpellID] then
                    db[selectedSpellID].imagePosX      = 0
                    db[selectedSpellID].imagePosY      = 0
                    db[selectedSpellID].imagePosAnchor = nil
                end
            end
            imgFrame:SetScale(1.0)
            if detailImageScaleDisplay then detailImageScaleDisplay:SetText("1.00") end
            imgFrame:ClearAllPoints()
            imgFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
            SetStatus("초기화되었습니다.", 0.3,1,0.3)
        end,
        timeout      = 0,
        whileDead    = true,
        hideOnEscape = true,
    }
    StaticPopup_Show("MSS_IMG_RESET_CONFIRM")
end)

-- ════════════════════════════════════════════════
--  탭3: 초시계
-- ════════════════════════════════════════════════
end

-- ════════════════════════════════════════════════
local function BuildTab3(tabPanels)
local P3 = tabPanels[3]

-- 활성화
detailTimerEnabledCheck = CreateFrame("CheckButton", nil, P3, "UICheckButtonTemplate")
detailTimerEnabledCheck:SetSize(22, 22)
detailTimerEnabledCheck:SetPoint("TOPLEFT", P3, "TOPLEFT", PL-2, -6)
detailTimerEnabledCheck:SetChecked(false)
local timerEnableLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
timerEnableLbl:SetPoint("LEFT", detailTimerEnabledCheck, "RIGHT", 2, 0)
timerEnableLbl:SetText("초시계 활성화")

local function MakeDividerP3(yOff)
    local d = P3:CreateTexture(nil, "ARTWORK")
    d:SetColorTexture(0.3,0.3,0.3,0.7)
    d:SetPoint("TOPLEFT",  P3, "TOPLEFT",  0, yOff)
    d:SetPoint("TOPRIGHT", P3, "TOPRIGHT", 0, yOff)
    d:SetHeight(1)
    return d
end

MakeDividerP3(-36)

-- 지속 시간 입력
local timerDurLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
timerDurLbl:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -44)
timerDurLbl:SetText("|cffaaaaaa스킬 지속 시간 (초) — 직접 입력 후 엔터|r")

detailTimerDurationEdit = CreateFrame("EditBox", nil, P3, "InputBoxTemplate")
detailTimerDurationEdit:SetSize(PW - PL*2, 20)
detailTimerDurationEdit:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -60)
detailTimerDurationEdit:SetAutoFocus(false)
detailTimerDurationEdit:SetNumeric(true)
detailTimerDurationEdit:SetMaxLetters(4)
detailTimerDurationEdit:SetText("0")

MakeDividerP3(-88)

-- 글씨 크기 + 색상 (한 줄에)
local timerFSLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
timerFSLbl:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -96)
timerFSLbl:SetText("|cffaaaaaa글씨 크기|r")

local timerColorLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
timerColorLbl:SetPoint("TOPLEFT", P3, "TOPLEFT", PL + 120, -96)
timerColorLbl:SetText("|cffaaaaaa글씨 색상|r")

local timerFSMinus = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
timerFSMinus:SetSize(26, 22)
timerFSMinus:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -112)
timerFSMinus:SetText("-")

detailTimerFontSizeDisplay = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailTimerFontSizeDisplay:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+28, -112)
detailTimerFontSizeDisplay:SetWidth(40)
detailTimerFontSizeDisplay:SetJustifyH("CENTER")
detailTimerFontSizeDisplay:SetText("22")

local timerFSPlus = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
timerFSPlus:SetSize(26, 22)
timerFSPlus:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+72, -112)
timerFSPlus:SetText("+")

timerFSMinus:SetScript("OnClick", function()
    local v = math.max(8, (tonumber(detailTimerFontSizeDisplay:GetText()) or 22) - 1)
    detailTimerFontSizeDisplay:SetText(tostring(v))
    AutoSave()
end)
timerFSPlus:SetScript("OnClick", function()
    local v = math.min(60, (tonumber(detailTimerFontSizeDisplay:GetText()) or 22) + 1)
    detailTimerFontSizeDisplay:SetText(tostring(v))
    AutoSave()
end)

-- 색상 스워치 + 버튼 (글씨 크기 옆)
local colorSwatch = P3:CreateTexture(nil, "OVERLAY")
colorSwatch:SetSize(18, 18)
colorSwatch:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+120, -112)
colorSwatch:SetColorTexture(1,1,1,1)

detailTimerColorBtn = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
detailTimerColorBtn:SetSize(PW - PL*2 - 140, 22)
detailTimerColorBtn:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+144, -112)
detailTimerColorBtn:SetText("색상 선택")

local function RefreshColorSwatch()
    colorSwatch:SetColorTexture(detailTimerColorR or 1, detailTimerColorG or 1, detailTimerColorB or 1, 1)
end
RefreshColorSwatch()

-- 색상 버튼 스크립트는 아래 AutoSave 블록에서 설정됨

MakeDividerP3(-144)

-- 배경 표시 + 불투명도 (한 줄에)
detailTimerBgCheck = CreateFrame("CheckButton", nil, P3, "UICheckButtonTemplate")
detailTimerBgCheck:SetSize(22, 22)
detailTimerBgCheck:SetPoint("TOPLEFT", P3, "TOPLEFT", PL-2, -150)
detailTimerBgCheck:SetChecked(false)
local timerBgLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
timerBgLbl:SetPoint("LEFT", detailTimerBgCheck, "RIGHT", 2, 0)
timerBgLbl:SetText("배경 표시")

local timerBgAlphaLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
timerBgAlphaLbl:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+120, -154)
timerBgAlphaLbl:SetText("|cffaaaaaa불투명도|r")

local timerBgAlphaMinus = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
timerBgAlphaMinus:SetSize(26, 22)
timerBgAlphaMinus:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+120, -170)
timerBgAlphaMinus:SetText("-")

detailTimerBgAlphaDisplay = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailTimerBgAlphaDisplay:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+148, -170)
detailTimerBgAlphaDisplay:SetWidth(44)
detailTimerBgAlphaDisplay:SetJustifyH("CENTER")
detailTimerBgAlphaDisplay:SetText("0.5")

local timerBgAlphaPlus = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
timerBgAlphaPlus:SetSize(26, 22)
timerBgAlphaPlus:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+196, -170)
timerBgAlphaPlus:SetText("+")

timerBgAlphaMinus:SetScript("OnClick", function()
    local v = math.max(0.0, math.floor(((tonumber(detailTimerBgAlphaDisplay:GetText()) or 0.5) - 0.1)*10+0.5)/10)
    detailTimerBgAlphaDisplay:SetText(string.format("%.1f", v))
    AutoSave()
end)
timerBgAlphaPlus:SetScript("OnClick", function()
    local v = math.min(1.0, math.floor(((tonumber(detailTimerBgAlphaDisplay:GetText()) or 0.5) + 0.1)*10+0.5)/10)
    detailTimerBgAlphaDisplay:SetText(string.format("%.1f", v))
    AutoSave()
end)

MakeDividerP3(-202)

-- ─ 앵커(기준점) 설명 ─────────────────────────────────────
local anchorDescLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
anchorDescLbl:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -210)
anchorDescLbl:SetWidth(PW - PL*2)
anchorDescLbl:SetText(
    "|cffaaaaaa[기준점]|r  여러 초시계가 동시에 활성화되면, " ..
    "가장 첫 번째(★) 초시계 위치가 기준점입니다. " ..
    "나머지는 지정 방향으로 차례로 나타납니다."
)

MakeDividerP3(-256)

-- ─ 간격 + 방향 (같은 줄) ────────────────────────────────
local gapLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
gapLbl:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -264)
gapLbl:SetText("|cffaaaaaa간격 (px)|r")

local dirLbl = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
dirLbl:SetPoint("TOPLEFT", P3, "TOPLEFT", PL + 130, -264)
dirLbl:SetText("|cffaaaaaa쌓이는 방향|r")

-- 간격 스테퍼
local gapMinus = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
gapMinus:SetSize(26, 22)
gapMinus:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -280)
gapMinus:SetText("-")

local gapDisplay = P3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
gapDisplay:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+28, -280)
gapDisplay:SetWidth(44)
gapDisplay:SetJustifyH("CENTER")
gapDisplay:SetText("36")

local gapPlus = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
gapPlus:SetSize(26, 22)
gapPlus:SetPoint("TOPLEFT", P3, "TOPLEFT", PL+76, -280)
gapPlus:SetText("+")

local function GetGapDisplay() return tonumber(gapDisplay:GetText()) or 36 end
local function SaveAnchorField(key, val)
    if not MoonlightSkillSoundDB._timerAnchor then
        MoonlightSkillSoundDB._timerAnchor = { posX=0, posY=150, anchor=nil, gap=36, growDir="down" }
    end
    MoonlightSkillSoundDB._timerAnchor[key] = val
end

gapMinus:SetScript("OnClick", function()
    local v = math.max(20, GetGapDisplay() - 2)
    gapDisplay:SetText(tostring(v)); SaveAnchorField("gap", v)
end)
gapPlus:SetScript("OnClick", function()
    local v = math.min(120, GetGapDisplay() + 2)
    gapDisplay:SetText(tostring(v)); SaveAnchorField("gap", v)
end)

-- 방향 토글 버튼 (↓ 아래로 / ↑ 위로)
local dirBtn = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
dirBtn:SetSize(PW - PL*2 - 130, 22)
dirBtn:SetPoint("TOPLEFT", P3, "TOPLEFT", PL + 130, -280)

local function RefreshDirBtn()
    local anc = GetTimerAnchor()
    if (anc.growDir or "down") == "down" then
        dirBtn:SetText("↓  아래로")
    else
        dirBtn:SetText("↑  위로")
    end
end
RefreshDirBtn()

dirBtn:SetScript("OnClick", function()
    local anc = GetTimerAnchor()
    local newDir = ((anc.growDir or "down") == "down") and "up" or "down"
    SaveAnchorField("growDir", newDir)
    RefreshDirBtn()
    -- 설명 문구도 갱신
    anchorDescLbl:SetText(
        "|cffaaaaaa[기준점]|r  여러 초시계가 동시에 활성화되면, " ..
        "가장 첫 번째(★) 초시계 위치가 기준점입니다. " ..
        "나머지는 지정 방향으로 차례로 나타납니다."
    )
    RelayoutTimers()
end)

MakeDividerP3(-312)

-- ─ 기준점 미리보기 버튼 (등록된 모든 스킬 동시 표시) ────
local timerPreviewing  = false
local previewFrames    = {}   -- 미리보기용 프레임 목록

detailTimerMoveBtn = CreateFrame("Button", nil, P3, "UIPanelButtonTemplate")
detailTimerMoveBtn:SetSize(PW - PL*2, 26)
detailTimerMoveBtn:SetPoint("TOPLEFT", P3, "TOPLEFT", PL, -322)
detailTimerMoveBtn:SetText("▶  기준점 미리보기 및 위치이동")
detailTimerMoveBtn:GetFontString():SetTextColor(0.3,1,0.7)

local function ClearPreviewFrames()
    for _, pf in ipairs(previewFrames) do
        if pf._previewTicker then pf._previewTicker:Cancel(); pf._previewTicker = nil end
        pf:SetMovable(false); pf:EnableMouse(false)
        pf:SetScript("OnDragStart", nil); pf:SetScript("OnDragStop", nil)
        pf:Hide(); pf:SetAlpha(0)
    end
    previewFrames = {}
end

local function TimerBtn_SetNormal()
    timerPreviewing = false
    timerMoveMode   = false
    detailTimerMoveBtn:SetText("▶  기준점 미리보기 및 위치이동")
    detailTimerMoveBtn:GetFontString():SetTextColor(0.3,1,0.7)
end

detailTimerMoveBtn:SetScript("OnClick", function()
    if not timerPreviewing then
        -- 등록된 스킬 목록 수집 (timerMap 기준 + 현재 편집 중인 스킬 포함)
        local anc     = GetTimerAnchor()
        local gap     = anc.gap     or 36
        local growDir = anc.growDir or "down"
        local dir     = (growDir == "up") and 1 or -1

        -- 미리보기에 표시할 항목 목록 구성
        -- timerMap에 있는 스킬들 + 현재 편집 중인 스킬(미저장이어도 포함)
        local previewList = {}  -- { label, dur, fsz, r,g,b, bgOn, bgA }

        -- 현재 편집 스킬 정보
        local curSpellName = (selectedSpellID and GetSpellInfo_Safe(selectedSpellID)) or "현재 스킬"
        local curDur  = tonumber(detailTimerDurationEdit and detailTimerDurationEdit:GetText()) or 0
        local curFsz  = tonumber(detailTimerFontSizeDisplay and detailTimerFontSizeDisplay:GetText()) or 22
        local curR, curG, curB = detailTimerColorR or 1, detailTimerColorG or 1, detailTimerColorB or 1
        local curBgOn = detailTimerBgCheck and detailTimerBgCheck:GetChecked()
        local curBgA  = tonumber(detailTimerBgAlphaDisplay and detailTimerBgAlphaDisplay:GetText()) or 0.5
        if curDur <= 0 then curDur = 10 end

        -- timerMap 스킬들 (정렬)
        local mapIDs = {}
        for sid in pairs(timerMap) do table.insert(mapIDs, sid) end
        table.sort(mapIDs)

        local addedCur = false
        for _, sid in ipairs(mapIDs) do
            local e = timerMap[sid]
            local isCur = (selectedSpellID and tonumber(selectedSpellID) == tonumber(sid))
            if isCur then
                -- 현재 편집 중인 스킬 → 편집 값 우선
                table.insert(previewList, { label=curSpellName, dur=curDur, fsz=curFsz,
                    r=curR, g=curG, b=curB, bgOn=curBgOn, bgA=curBgA })
                addedCur = true
            else
                table.insert(previewList, { label=e.spellName, dur=e.duration, fsz=e.fontSz,
                    r=e.r, g=e.g, b=e.b, bgOn=e.bgEnabled, bgA=e.bgAlpha })
            end
        end
        -- 현재 스킬이 timerMap에 없으면 첫 번째로 추가
        if not addedCur and selectedSpellID then
            table.insert(previewList, 1, { label=curSpellName, dur=curDur, fsz=curFsz,
                r=curR, g=curG, b=curB, bgOn=curBgOn, bgA=curBgA })
        end
        -- 최소 1개 보장
        if #previewList == 0 then
            table.insert(previewList, { label="스킬명", dur=10, fsz=22, r=1,g=1,b=1, bgOn=false, bgA=0.5 })
        end

        timerMoveMode   = true
        timerPreviewing = true
        local font = GameFontNormal:GetFont()

        for i, item in ipairs(previewList) do
            local pf = GetPooledTimerFrame()
            local pt = pf.timerText
            local offsetY = dir * (i - 1) * gap

            pf:ClearAllPoints()
            if anc.anchor == "BOTTOMLEFT" then
                pf:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", anc.posX, anc.posY + offsetY)
            else
                pf:SetPoint("CENTER", UIParent, "CENTER", anc.posX, anc.posY + offsetY)
            end

            pt:SetFont(font, item.fsz, "OUTLINE")
            pt:SetTextColor(item.r, item.g, item.b)

            if item.bgOn then
                pf:SetBackdropColor(0,0,0, item.bgA)
                pf:SetBackdropBorderColor(0.3,0.3,0.3,0.8)
            else
                pf:SetBackdropColor(0,0,0,0)
                pf:SetBackdropBorderColor(0,0,0,0)
            end

            -- 첫 번째 스킬에 ★(앵커 기준) 표시
            local prefix = (i == 1) and "★ " or "  "
            local startTime = GetTime()
            local dur = item.dur
            local label = item.label

            local function updatePreview()
                local rem = math.max(0, dur - (GetTime() - startTime))
                pt:SetText(string.format("%s%s - %d초", prefix, label, math.ceil(rem)))
                local tw = pt:GetStringWidth() + 20
                local th = pt:GetStringHeight() + 14
                pf:SetSize(math.max(tw,80), math.max(th,30))
            end

            updatePreview()
            pf:SetAlpha(1); pf:Show()

            local ticker = C_Timer.NewTicker(0.5, function()
                if not timerPreviewing then return end
                updatePreview()
            end)
            pf._previewTicker = ticker

            -- 첫 번째 프레임만 드래그 가능 (앵커 이동)
            if i == 1 then
                pf:SetMovable(true); pf:EnableMouse(true)
                pf:RegisterForDrag("LeftButton")
                pf:SetScript("OnDragStart", pf.StartMoving)
                pf:SetScript("OnDragStop", function(self)
                    self:StopMovingOrSizing()
                    if not MoonlightSkillSoundDB._timerAnchor then
                        MoonlightSkillSoundDB._timerAnchor = { posX=0, posY=150, anchor=nil, gap=36, growDir="down" }
                    end
                    MoonlightSkillSoundDB._timerAnchor.posX   = self:GetLeft()
                    MoonlightSkillSoundDB._timerAnchor.posY   = self:GetBottom()
                    MoonlightSkillSoundDB._timerAnchor.anchor = "BOTTOMLEFT"
                    -- 나머지 미리보기 프레임도 같이 재배치
                    local newAnc = GetTimerAnchor()
                    local newGap = newAnc.gap or 36
                    local newDir = ((newAnc.growDir or "down") == "up") and 1 or -1
                    for j, opf in ipairs(previewFrames) do
                        local offsetJ = newDir * (j - 1) * newGap
                        opf:ClearAllPoints()
                        opf:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT",
                            newAnc.posX, newAnc.posY + offsetJ)
                    end
                    RebuildTimerMap()
                end)
            end

            table.insert(previewFrames, pf)
        end

        detailTimerMoveBtn:SetText("|cffff9999■  ★ 드래그로 기준점 이동 중… (클릭해서 완료)|r")
        detailTimerMoveBtn:GetFontString():SetTextColor(1,1,1)
    else
        ClearPreviewFrames()
        TimerBtn_SetNormal()
        SetStatus("기준점 위치가 저장되었습니다.", 0.3,1,0.7)
    end
end)

-- DB값 초기 반영 (ADDON_LOADED 이후)
C_Timer.After(0, function()
    local a = MoonlightSkillSoundDB and MoonlightSkillSoundDB._timerAnchor
    if a then
        gapDisplay:SetText(tostring(a.gap or 36))
        RefreshDirBtn()
    end
end)

-- 탭 초기화
end

-- ════════════════════════════════════════════════
local function BuildFinalize()
-- SaveAllFields / AutoSave는 모듈 레벨에서 정의됨

-- 초시계 탭 컨트롤에 AutoSave 연결
detailTimerEnabledCheck:SetScript("OnClick",   AutoSave)
detailTimerBgCheck:SetScript("OnClick",        AutoSave)
detailTimerColorBtn:SetScript("OnClick", function()  -- 색상 버튼은 피커 열기 + 피커 닫힐 때 저장
    local r, g, b = detailTimerColorR or 1, detailTimerColorG or 1, detailTimerColorB or 1
    if ColorPickerFrame.SetupColorPickerAndShow then
        ColorPickerFrame:SetupColorPickerAndShow({
            r=r, g=g, b=b, hasOpacity=false,
            swatchFunc = function()
                detailTimerColorR, detailTimerColorG, detailTimerColorB = ColorPickerFrame:GetColorRGB()
                RefreshColorSwatch(); AutoSave()
            end,
            cancelFunc = function(prev)
                detailTimerColorR, detailTimerColorG, detailTimerColorB = prev.r, prev.g, prev.b
                RefreshColorSwatch()
            end,
            previousValues = { r=r, g=g, b=b },
        })
    else
        ColorPickerFrame:Hide()
        ColorPickerFrame.func = function()
            detailTimerColorR, detailTimerColorG, detailTimerColorB = ColorPickerFrame:GetColorRGB()
            RefreshColorSwatch(); AutoSave()
        end
        ColorPickerFrame.cancelFunc = function()
            detailTimerColorR, detailTimerColorG, detailTimerColorB = r, g, b
            RefreshColorSwatch()
        end
        ColorPickerFrame:SetColorRGB(r, g, b)
        ColorPickerFrame:Show()
    end
end)

-- 이미지 탭 체크박스에도 AutoSave 연결
if detailImageEnabledCheck then
    detailImageEnabledCheck:SetScript("OnClick", AutoSave)
end
if detailImageFadeCheck then
    detailImageFadeCheck:SetScript("OnClick", AutoSave)
end


-- 지속 시간 EditBox: 엔터 시 포커스 해제 + 자동 저장
detailTimerDurationEdit:SetScript("OnEnterPressed", function(self)
    self:ClearFocus()
    AutoSave()
end)
detailTimerDurationEdit:SetScript("OnEscapePressed", function(self)
    self:ClearFocus()
end)

-------------------------------------------------------
-- 버튼 스크립트
-------------------------------------------------------
detailSaveBtn:SetScript("OnClick", function()
    SaveAllFields(false)
end)

detailTestBtn:SetScript("OnClick", function()
    local path = detailPathEdit:GetText() or ""
    if path == "" then SetStatus("재생할 사운드 경로를 입력하세요.", 1,0.3,0.3); return end
    PlaySoundFile(path, selectedChannel)
    SetStatus("재생 중: "..path, 0.4,1,0.4)
end)

detailDeleteBtn:SetScript("OnClick", function()
    if not selectedClassID or not selectedSpellID then return end
    MoonlightSkillSoundDB[selectedClassID][selectedSpellID] = nil
    selectedSpellID = nil
    RebuildRuntimeMap(); RefreshSkillColumn(selectedClassID)
    LoadDetailPanel(selectedClassID, nil)
    SetStatus("삭제되었습니다.", 1,0.7,0.3)
end)

-------------------------------------------------------
-- 하단: 초기화 드롭다운 (버튼 1개로 통합)
-------------------------------------------------------
local bottomDiv = mainFrame:CreateTexture(nil, "ARTWORK")
bottomDiv:SetColorTexture(0.35,0.35,0.35,0.5)
bottomDiv:SetPoint("BOTTOMLEFT",  mainFrame, "BOTTOMLEFT",  16, 50)
bottomDiv:SetPoint("BOTTOMRIGHT", mainFrame, "BOTTOMRIGHT", -16, 50)
bottomDiv:SetHeight(1)

local resetActions = {
    { label="현재 스킬 프리셋으로 초기화", func=function()
        if not selectedClassID then SetStatus("먼저 직업을 선택하세요.",1,0.3,0.3); return end
        if not selectedSpellID then SetStatus("먼저 스킬을 선택하세요.",1,0.3,0.3); return end
        if not ResetSpellToPreset(selectedClassID, selectedSpellID) then
            SetStatus("해당 스킬의 프리셋 데이터가 없습니다.",1,0.3,0.3); return end
        RebuildRuntimeMap(); RefreshSkillColumn(selectedClassID)
        LoadDetailPanel(selectedClassID, selectedSpellID)
        SetStatus("현재 스킬을 프리셋으로 초기화했습니다.",0.3,1,0.3)
    end },
    { label="현재 직업 프리셋으로 초기화", func=function()
        if not selectedClassID then SetStatus("먼저 직업을 선택하세요.",1,0.3,0.3); return end
        if not ResetClassToPreset(selectedClassID) then
            SetStatus("해당 직업의 프리셋 데이터가 없습니다.",1,0.3,0.3); return end
        RebuildRuntimeMap(); RefreshSkillColumn(selectedClassID)
        LoadDetailPanel(selectedClassID, nil)
        SetStatus("현재 직업 프리셋으로 초기화되었습니다.",0.3,1,0.3)
    end },
    { label="|cffff8888모든 직업 프리셋으로 초기화|r", func=function()
        if not MoonlightSkillSoundDefaults then
            SetStatus("프리셋 데이터가 없습니다.",1,0.3,0.3); return end
        for classID in pairs(MoonlightSkillSoundDefaults) do ResetClassToPreset(classID) end
        RebuildRuntimeMap(); RefreshSkillColumn(selectedClassID)
        LoadDetailPanel(selectedClassID, nil)
        SetStatus("모든 직업이 프리셋으로 초기화되었습니다.",0.3,1,0.3)
    end },
}

local resetPopup = CreateFrame("Frame", nil, mainFrame, "BackdropTemplate")
resetPopup:SetSize(240, #resetActions * 26)
resetPopup:SetFrameStrata("TOOLTIP"); resetPopup:SetFrameLevel(120)
resetPopup:SetBackdrop({
    bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    tile = false, edgeSize = 1,
})
resetPopup:SetBackdropBorderColor(0.5,0.45,0.1,1)
resetPopup:Hide()

for i, act in ipairs(resetActions) do
    local rb = CreateFrame("Button", nil, resetPopup)
    rb:SetSize(240, 26)
    rb:SetPoint("TOPLEFT", resetPopup, "TOPLEFT", 0, -(i-1)*26)
    local rbBg = rb:CreateTexture(nil, "BACKGROUND")
    rbBg:SetAllPoints(); rbBg:SetColorTexture(1,1,1,0)
    local rbLbl = rb:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    rbLbl:SetPoint("LEFT", rb, "LEFT", 8, 0); rbLbl:SetText(act.label)
    rb:SetScript("OnEnter", function() rbBg:SetColorTexture(1,0.82,0,0.15) end)
    rb:SetScript("OnLeave", function() rbBg:SetColorTexture(1,1,1,0) end)
    local fn = act.func
    rb:SetScript("OnClick", function() resetPopup:Hide(); fn() end)
end

local resetMenuBtn = CreateFrame("Button", nil, mainFrame, "UIPanelButtonTemplate")
resetMenuBtn:SetSize(200, 26)
resetMenuBtn:SetPoint("BOTTOMLEFT", mainFrame, "BOTTOMLEFT", 16, 16)
resetMenuBtn:SetText("초기화  ▾")
resetMenuBtn:SetScript("OnClick", function(self)
    if resetPopup:IsShown() then resetPopup:Hide()
    else
        resetPopup:ClearAllPoints()
        resetPopup:SetPoint("BOTTOMLEFT", self, "TOPLEFT", 0, 2)
        resetPopup:Show()
    end
end)

local verFS = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
verFS:SetPoint("BOTTOMRIGHT", mainFrame, "BOTTOMRIGHT", -16, 20)
verFS:SetTextColor(0.4,0.4,0.4); verFS:SetText("/mss")
end

-------------------------------------------------------
-- UI 빌드
-------------------------------------------------------
local function BuildUI()

    -- ── 메인 프레임 (컴팩트) ──────────────────────────
    mainFrame = CreateFrame("Frame", "MoonlightSkillSoundFrame", UIParent, "BackdropTemplate")
    mainFrame:SetSize(860, 660)
    mainFrame:SetPoint("CENTER")
    mainFrame:SetFrameStrata("DIALOG")
    mainFrame:SetFrameLevel(20)
    mainFrame:SetMovable(true)
    mainFrame:EnableMouse(true)
    mainFrame:RegisterForDrag("LeftButton")
    mainFrame:SetClampedToScreen(true)
    mainFrame:Hide()
    mainFrame:SetBackdrop({
        bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left=8, right=8, top=8, bottom=8 },
    })
    mainFrame:SetScript("OnDragStart", mainFrame.StartMoving)
    mainFrame:SetScript("OnDragStop",  mainFrame.StopMovingOrSizing)
    table.insert(UISpecialFrames, "MoonlightSkillSoundFrame")

    local titleFS = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    titleFS:SetPoint("TOPLEFT", 20, -16)
    titleFS:SetText("|cffffcc00Moonlight|r SkillSound")

    local subFS = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    subFS:SetPoint("LEFT", titleFS, "RIGHT", 10, -1)
    subFS:SetTextColor(0.45, 0.45, 0.45)
    subFS:SetText("/mss")

    local closeBtn = CreateFrame("Button", nil, mainFrame, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -6, -6)

    local topDiv = mainFrame:CreateTexture(nil, "ARTWORK")
    topDiv:SetColorTexture(0.35, 0.35, 0.35, 0.5)
    topDiv:SetPoint("TOPLEFT",  mainFrame, "TOPLEFT",  16, -40)
    topDiv:SetPoint("TOPRIGHT", mainFrame, "TOPRIGHT", -16, -40)
    topDiv:SetHeight(1)

    -- 헬퍼 함수: UI_MakeBox / UI_MakeDivider / UI_MakeDropdown (모듈 레벨)
    local function MakeBox(px, py, pw, ph)  return UI_MakeBox(mainFrame, px, py, pw, ph) end
    local function MakeDivider(p, y)        return UI_MakeDivider(p, y) end
    local function MakeDropdown(...)        return UI_MakeDropdown(...) end

    -- ════════════════════════════════════════════════
    --  컬럼 1: 직업
    -- ════════════════════════════════════════════════
    local box1 = MakeBox(X1, COL_TOP, COL1_W, COL_H)

    local t1 = box1:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    t1:SetPoint("TOPLEFT", box1, "TOPLEFT", 8, -7)
    t1:SetText("|cffaaaaaa직  업|r")
    MakeDivider(box1, -20)

    for i, cls in ipairs(CLASS_LIST) do
        local row = CreateFrame("Button", nil, box1)
        row:SetSize(COL1_W - 4, 24)
        if i == 1 then row:SetPoint("TOPLEFT", box1, "TOPLEFT", 2, -24)
        else row:SetPoint("TOPLEFT", classRows[i-1], "BOTTOMLEFT", 0, -1) end

        row.bg = row:CreateTexture(nil, "BACKGROUND")
        row.bg:SetAllPoints(); row.bg:SetColorTexture(1,1,1,0.04)

        local bar = row:CreateTexture(nil, "ARTWORK")
        bar:SetSize(2, 14); bar:SetPoint("LEFT", row, "LEFT", 4, 0)
        local cr, cg, cb = HexColor(cls.color)
        bar:SetColorTexture(cr, cg, cb, 1)

        local lbl = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        lbl:SetPoint("LEFT", bar, "RIGHT", 5, 0)
        lbl:SetText(cls.name); lbl:SetTextColor(cr, cg, cb)

        row.classID = cls.id
        row:SetScript("OnClick", OnClassRowClick)
        row:SetScript("OnEnter", function(s) s.bg:SetColorTexture(1,1,1,0.1) end)
        row:SetScript("OnLeave", function(s)
            if selectedClassID == s.classID then s.bg:SetColorTexture(1,0.82,0,0.15)
            else s.bg:SetColorTexture(1,1,1,0.04) end
        end)
        classRows[i] = row
    end

    -- ════════════════════════════════════════════════
    --  컬럼 2: 스킬
    -- ════════════════════════════════════════════════
    local box2 = MakeBox(X2, COL_TOP, COL2_W, COL_H)

    local t2 = box2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    t2:SetPoint("TOPLEFT", box2, "TOPLEFT", 8, -7)
    t2:SetText("|cffaaaaaa스  킬|r")
    MakeDivider(box2, -20)

    local addEdit = CreateFrame("EditBox", nil, box2, "InputBoxTemplate")
    addEdit:SetSize(COL2_W - 62, 20)
    addEdit:SetPoint("TOPLEFT", box2, "TOPLEFT", 6, -30)
    addEdit:SetAutoFocus(false); addEdit:SetNumeric(true)

    local addHint = box2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    addHint:SetPoint("LEFT", addEdit, "LEFT", 4, 0)
    addHint:SetTextColor(0.45, 0.45, 0.45); addHint:SetText("Spell ID 입력…")
    addEdit:SetScript("OnTextChanged", function(self) addHint:SetShown(self:GetText() == "") end)
    addHint:SetShown(true)

    local addBtn = CreateFrame("Button", nil, box2, "UIPanelButtonTemplate")
    addBtn:SetSize(46, 20); addBtn:SetPoint("LEFT", addEdit, "RIGHT", 4, 0)
    addBtn:SetText("추가")

    addBtn:SetScript("OnClick", function()
        local spellID = tonumber(addEdit:GetText())
        if not spellID or spellID <= 0 then SetStatus("올바른 Spell ID를 입력하세요.", 1,0.3,0.3); return end
        if not selectedClassID then SetStatus("먼저 직업을 선택하세요.", 1,0.3,0.3); return end
        local nm = GetSpellInfo_Safe(spellID)
        if not nm then SetStatus("존재하지 않는 Spell ID입니다.", 1,0.3,0.3); return end
        local db = MoonlightSkillSoundDB[selectedClassID]
        if db[spellID] then SetStatus("이미 추가된 스킬입니다.", 1,0.7,0.3)
        else
            db[spellID] = { soundPath="", soundChannel="Master", enabled=true, triggerType="instant",
                            imagePath="", imageEnabled=false, imageDuration=3.0,
                            imageScale=1.0, imagePosX=0, imagePosY=0, imagePosAnchor=nil }
            SetStatus("스킬이 추가되었습니다.", 0.3,1,0.3)
        end
        addEdit:SetText(""); addEdit:SetFocus()
        selectedSpellID = spellID
        RefreshSkillColumn(selectedClassID)
        LoadDetailPanel(selectedClassID, spellID)
    end)
    addEdit:SetScript("OnEnterPressed", function() addBtn:Click() end)

    local skillScroll = CreateFrame("ScrollFrame", nil, box2, "UIPanelScrollFrameTemplate")
    skillScroll:SetPoint("TOPLEFT",     box2, "TOPLEFT",     4, -56)
    skillScroll:SetPoint("BOTTOMRIGHT", box2, "BOTTOMRIGHT", -22, 4)

    skillScrollContent = CreateFrame("Frame", nil, skillScroll)
    skillScrollContent:SetSize(COL2_W - 28, 1)
    skillScroll:SetScrollChild(skillScrollContent)

    -- ════════════════════════════════════════════════
    --  컬럼 3: 사운드 설정 (탭 포함)
    -- ════════════════════════════════════════════════
    local box3 = MakeBox(X3, COL_TOP, COL3_W, COL_H)

    detailSpellIcon = box3:CreateTexture(nil, "ARTWORK")
    detailSpellIcon:SetSize(32, 32); detailSpellIcon:SetPoint("TOPLEFT", box3, "TOPLEFT", 8, -8)
    detailSpellIcon:SetTexture(134400); detailSpellIcon:SetTexCoord(0.08,0.92,0.08,0.92)

    detailSpellName = box3:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailSpellName:SetPoint("LEFT", detailSpellIcon, "RIGHT", 6, 0)
    detailSpellName:SetPoint("RIGHT", box3, "RIGHT", -8, 0)
    detailSpellName:SetJustifyH("LEFT")
    detailSpellName:SetText("|cff666666스킬을 선택하세요|r")

    MakeDivider(box3, -46)

    -- 탭 바
    local tabPanels = {}
    local tabBtns   = {}

    local function ShowDetailTab(idx)
        for i, tb in ipairs(tabBtns) do
            if i == idx then
                tb:SetBackdropColor(0.15,0.15,0.20,1)
                tb:SetBackdropBorderColor(0.65,0.60,0.25,0.9)
                tb.lbl:SetTextColor(1,0.85,0.2)
            else
                tb:SetBackdropColor(0.06,0.06,0.10,0.9)
                tb:SetBackdropBorderColor(0.25,0.25,0.25,0.7)
                tb.lbl:SetTextColor(0.55,0.55,0.55)
            end
        end
        for i, p in ipairs(tabPanels) do
            if i == idx then p:Show() else p:Hide() end
        end
    end

    local tabLabels = { "사운드", "이미지", "초시계" }
    for i, lbl in ipairs(tabLabels) do
        local tb = CreateFrame("Button", nil, box3, "BackdropTemplate")
        tb:SetSize(TAB_W, TAB_H)
        tb:SetPoint("TOPLEFT", box3, "TOPLEFT", (i-1)*TAB_W, TAB_Y)
        tb:SetBackdrop({
            bgFile   = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            tile = false, edgeSize = 1,
        })
        tb:SetBackdropColor(0.06,0.06,0.10,0.9)
        tb:SetBackdropBorderColor(0.25,0.25,0.25,0.7)
        -- BackdropTemplate 버튼은 GetFontString()이 없으므로 직접 저장
        local tbLbl = tb:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        tbLbl:SetPoint("CENTER", tb, "CENTER", 0, 0)
        tbLbl:SetText(lbl); tbLbl:SetTextColor(0.55,0.55,0.55)
        tb.lbl = tbLbl
        local idx = i
        tb:SetScript("OnClick", function() ShowDetailTab(idx) end)
        tabBtns[i] = tb

        local panel = CreateFrame("Frame", nil, box3)
        panel:SetPoint("TOPLEFT",     box3, "TOPLEFT",     0, TAB_Y - TAB_H)
        panel:SetPoint("BOTTOMRIGHT", box3, "BOTTOMRIGHT", 0, 0)
        panel:Hide()
        tabPanels[i] = panel
    end

    -- ════════════════════════════════════════════════
    --  탭1: 사운드
    -- ════════════════════════════════════════════════
    -- 탭별 빌드 (함수 분리로 upvalue 제한 회피)
    BuildTab1(tabPanels)
    BuildTab2(tabPanels)
    BuildTab3(tabPanels)
    BuildFinalize()
end


-------------------------------------------------------
-- 창 열기 / 닫기
-------------------------------------------------------
local function OpenUI()
    if not mainFrame then BuildUI() end

    if not selectedClassID then
        local _, classFile = UnitClass("player")
        for i, c in ipairs(CLASS_LIST) do
            if c.nameEN == classFile then
                selectedClassID = c.id
                if classRows[i] then OnClassRowClick(classRows[i]) end
                break
            end
        end
    end
    mainFrame:Show()
end

local function ToggleUI()
    if not mainFrame then BuildUI() end
    if mainFrame:IsShown() then mainFrame:Hide() else OpenUI() end
end

-------------------------------------------------------
-- 게임 내 설정 패널 등록
-------------------------------------------------------
local function RegisterSettingsPanel()
    local panel = CreateFrame("Frame", "MoonlightSkillSoundSettingsPanel")
    panel.name  = "!Moonlight_스킬사운드"

    local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -16)
    title:SetText("|cffffcc00Moonlight|r 스킬사운드")

    local desc = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -10)
    desc:SetText("스킬 사용 시 지정된 사운드 파일을 재생합니다.")
    desc:SetTextColor(0.8, 0.8, 0.8)

    local openBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    openBtn:SetSize(220, 28)
    openBtn:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", 0, -20)
    openBtn:SetText("스킬사운드 설정 창 열기")
    openBtn:SetScript("OnClick", function()
        if SettingsPanel and SettingsPanel:IsShown() then
            HideUIPanel(SettingsPanel)
        elseif InterfaceOptionsFrame and InterfaceOptionsFrame:IsShown() then
            HideUIPanel(InterfaceOptionsFrame)
        end
        OpenUI()
    end)

    local slashLbl = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    slashLbl:SetPoint("TOPLEFT", openBtn, "BOTTOMLEFT", 0, -10)
    slashLbl:SetText("슬래시 명령어:  /mss")
    slashLbl:SetTextColor(0.6, 0.6, 0.6)

    if Settings and Settings.RegisterCanvasLayoutCategory then
        local cat = Settings.RegisterCanvasLayoutCategory(panel, "!Moonlight SkillSound")
        Settings.RegisterAddOnCategory(cat)
    elseif InterfaceOptions_AddCategory then
        InterfaceOptions_AddCategory(panel)
    end
end

-------------------------------------------------------
-- 슬래시 명령어
-------------------------------------------------------
SLASH_MSS1 = "/mss"
SLASH_MSS2 = "/ㅡㄴㄴ"
SlashCmdList["MSS"] = function() ToggleUI() end
SlashCmdList["ㅡㄴㄴ"] = function() ToggleUI() end
-------------------------------------------------------
-- 이벤트
-------------------------------------------------------
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED",     "player")
eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_START",         "player")
eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player")

eventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local name = ...
        if name == ADDON_NAME then
            InitDB()
            RegisterSettingsPanel()
        end

    elseif event == "PLAYER_LOGIN" then
        RebuildRuntimeMap()
        StartRecentPlayedCleanup()

    elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
        local unit, _, spellID = ...
        if unit == "player" and spellID then
            if not castMap[spellID] and not channelMap[spellID] then
                PlaySkillSound(spellID, "instant")
            end
            ShowSkillImage(spellID)
            ShowSkillTimer(spellID)
        end

    elseif event == "UNIT_SPELLCAST_START" then
        local unit, _, spellID = ...
        if unit == "player" and spellID then
            -- channelMap에 있는 스킬은 UNIT_SPELLCAST_CHANNEL_START에서 처리하므로 제외
            if not instantMap[spellID] and not channelMap[spellID] then
                PlaySkillSound(spellID, "cast")
            end
            ShowSkillImage(spellID)
            ShowSkillTimer(spellID)
        end

    elseif event == "UNIT_SPELLCAST_CHANNEL_START" then
        local unit, _, spellID = ...
        if unit == "player" and spellID then
            PlaySkillSound(spellID, "channel")
            ShowSkillImage(spellID)
            ShowSkillTimer(spellID)
        end
    end
end)
