-------------------------------------------------------
-- 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,
                }
            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
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()

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

local imgHideTimer  = nil
local imgFadeTicker = nil

local function ImageFadeOut()
    if imgFadeTicker then imgFadeTicker:Cancel() end
    local start = GetTime()
    local FADE  = 0.4
    imgFadeTicker = C_Timer.NewTicker(0.016, function(t)
        local prog = math.min((GetTime() - start) / FADE, 1)
        imgTexture:SetAlpha(1 - prog)
        if prog >= 1 then
            imgFrame:Hide()
            imgTexture:SetAlpha(0)
            t:Cancel()
        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

    imgFrame:ClearAllPoints()
    if entry.posAnchor == "BOTTOMLEFT" then
        imgFrame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", entry.posX, entry.posY)
    else
        imgFrame:SetPoint("CENTER", UIParent, "CENTER", entry.posX, entry.posY)
    end

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

    if imgHideTimer  then imgHideTimer:Cancel();  imgHideTimer  = nil end
    if imgFadeTicker then imgFadeTicker:Cancel(); imgFadeTicker = nil end

    imgFrame:Show()
    imgTexture:SetAlpha(0)
    local start = GetTime()
    local FADE  = 0.3
    imgFadeTicker = C_Timer.NewTicker(0.016, function(t)
        local prog = math.min((GetTime() - start) / FADE, 1)
        imgTexture:SetAlpha(prog)
        if prog >= 1 then
            imgTexture:SetAlpha(1)
            t:Cancel()
            imgFadeTicker = nil
        end
    end)

    imgHideTimer = C_Timer.NewTimer(entry.duration, function()
        imgHideTimer = nil
        if not imgMoveMode then ImageFadeOut() 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,  -- 프레임 기본 크기 (배율은 SetScale로 적용)
                    height    = 256,
                    posX      = data.imagePosX     or 0,
                    posY      = data.imagePosY     or 0,
                    posAnchor = data.imagePosAnchor,
                }
            end
        end
    end
    loadImg(MoonlightSkillSoundDB[cls.id])
    loadImg(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()
end

-- ★ 중복 재생 방지: 같은 스킬이 0.2초 이내에 두 번 재생되는 것을 막음
local recentPlayed = {}

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

    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 상태 변수
-------------------------------------------------------
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
-- imgMoveMode는 상단 이미지 블록에서 이미 선언됨

-------------------------------------------------------
-- 헬퍼
-------------------------------------------------------
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,
        }
    end
    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
    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
    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 빌드
-------------------------------------------------------
local function BuildUI()

    mainFrame = CreateFrame("Frame", "MoonlightSkillSoundFrame", UIParent, "BackdropTemplate")
    mainFrame:SetSize(860, 800)
    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.5, 0.5, 0.5)
    subFS:SetText("스킬 사용 시 사운드를 재생합니다  |  /mss")

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

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

    local X1, X2, X3 = 16, 179, 397
    local COL_TOP    = -48
    local COL_H      = 730
    local COL1_W     = 155
    local COL2_W     = 210
    local COL3_W     = 447

    local function MakeBox(px, py, pw, ph)
        local f = CreateFrame("Frame", nil, mainFrame, "BackdropTemplate")
        f:SetPoint("TOPLEFT", mainFrame, "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.28)
        f:SetBackdropBorderColor(0.38, 0.38, 0.38, 0.9)
        return f
    end

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

    local t1 = box1:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    t1:SetPoint("TOPLEFT", box1, "TOPLEFT", 8, -8)
    t1:SetText("|cffffcc00직  업|r")

    local hLine1 = box1:CreateTexture(nil, "ARTWORK")
    hLine1:SetColorTexture(0.38, 0.38, 0.38, 0.6)
    hLine1:SetPoint("TOPLEFT",  box1, "TOPLEFT",  1, -24)
    hLine1:SetPoint("TOPRIGHT", box1, "TOPRIGHT", -1, -24)
    hLine1:SetHeight(1)

    for i, cls in ipairs(CLASS_LIST) do
        local row = CreateFrame("Button", nil, box1)
        row:SetSize(COL1_W - 6, 26)
        if i == 1 then
            row:SetPoint("TOPLEFT", box1, "TOPLEFT", 3, -28)
        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(3, 18)
        bar:SetPoint("LEFT", row, "LEFT", 3, 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", "GameFontNormal")
    t2:SetPoint("TOPLEFT", box2, "TOPLEFT", 8, -8)
    t2:SetText("|cffffcc00스  킬|r")

    local hLine2 = box2:CreateTexture(nil, "ARTWORK")
    hLine2:SetColorTexture(0.38, 0.38, 0.38, 0.6)
    hLine2:SetPoint("TOPLEFT",  box2, "TOPLEFT",  1, -24)
    hLine2:SetPoint("TOPRIGHT", box2, "TOPRIGHT", -1, -24)
    hLine2:SetHeight(1)

    local addLbl = box2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    addLbl:SetPoint("TOPLEFT", box2, "TOPLEFT", 8, -30)
    addLbl:SetTextColor(0.6, 0.6, 0.6)
    addLbl:SetText("Spell ID:")

    local addEdit = CreateFrame("EditBox", nil, box2, "InputBoxTemplate")
    addEdit:SetSize(110, 20)
    addEdit:SetPoint("TOPLEFT", box2, "TOPLEFT", 8, -47)
    addEdit:SetAutoFocus(false)
    addEdit:SetNumeric(true)

    local addBtn = CreateFrame("Button", nil, box2, "UIPanelButtonTemplate")
    addBtn:SetSize(56, 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",     6, -72)
    skillScroll:SetPoint("BOTTOMRIGHT", box2, "BOTTOMRIGHT", -24, 6)

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

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

    local t3 = box3:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    t3:SetPoint("TOPLEFT", box3, "TOPLEFT", 8, -8)
    t3:SetText("|cffffcc00사운드 설정|r")

    local hLine3 = box3:CreateTexture(nil, "ARTWORK")
    hLine3:SetColorTexture(0.38, 0.38, 0.38, 0.6)
    hLine3:SetPoint("TOPLEFT",  box3, "TOPLEFT",  1, -24)
    hLine3:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -1, -24)
    hLine3:SetHeight(1)

    detailSpellIcon = box3:CreateTexture(nil, "ARTWORK")
    detailSpellIcon:SetSize(38, 38)
    detailSpellIcon:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -34)
    detailSpellIcon:SetTexture(134400)
    detailSpellIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

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

    local d1 = box3:CreateTexture(nil, "ARTWORK")
    d1:SetColorTexture(0.38, 0.38, 0.38, 0.5)
    d1:SetPoint("TOPLEFT",  box3, "TOPLEFT",  10, -80)
    d1:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -10, -80)
    d1:SetHeight(1)

    local pathLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    pathLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -92)
    pathLbl:SetText("사운드 파일 경로")

    local pathHint = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    pathHint:SetPoint("LEFT", pathLbl, "RIGHT", 8, 0)
    pathHint:SetTextColor(0.5, 0.5, 0.5)
    pathHint:SetText("(예: Interface\\AddOns\\MyAddon\\sound.ogg)")

    detailPathEdit = CreateFrame("EditBox", nil, box3, "InputBoxTemplate")
    detailPathEdit:SetSize(COL3_W - 20, 22)
    detailPathEdit:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -110)
    detailPathEdit:SetAutoFocus(false)
    detailPathEdit:SetMaxLetters(512)

    detailSMLabel = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    detailSMLabel:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -138)
    detailSMLabel:SetTextColor(0.4, 0.8, 1)
    detailSMLabel:SetText("또는 SharedMedia 라이브러리에서 선택:")

    detailSMHint = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    detailSMHint:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -138)
    detailSMHint:SetTextColor(0.45, 0.45, 0.45)
    detailSMHint:SetText("SharedMedia 라이브러리가 설치되어 있지 않습니다.")
    detailSMHint:Hide()

    detailSMPickerBtn = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    detailSMPickerBtn:SetSize(COL3_W - 20, 24)
    detailSMPickerBtn:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -154)
    detailSMPickerBtn:SetText("선택...")

    -- ── SharedMedia 스크롤 팝업 패널 ────────────────────────
    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 rows  = {}
        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(rows) do r:Hide() end

            for i, name in ipairs(filtered) do
                local row = rows[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)
                    rows[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()

    local d2 = box3:CreateTexture(nil, "ARTWORK")
    d2:SetColorTexture(0.38, 0.38, 0.38, 0.5)
    d2:SetPoint("TOPLEFT",  box3, "TOPLEFT",  10, -194)
    d2:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -10, -194)
    d2:SetHeight(1)

    local chanLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    chanLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -206)
    chanLbl:SetText("사운드 채널")

    detailChannelDropdown = CreateFrame("Frame", "MSSChannelDropdown", box3, "UIDropDownMenuTemplate")
    detailChannelDropdown:SetPoint("TOPLEFT", box3, "TOPLEFT", -6, -224)

    UIDropDownMenu_Initialize(detailChannelDropdown, function()
        for _, ch in ipairs(CHANNELS) do
            local info   = UIDropDownMenu_CreateInfo()
            info.text    = ch
            info.value   = ch
            info.checked = (selectedChannel == ch)
            info.func    = function() SetChannel(ch) end
            UIDropDownMenu_AddButton(info)
        end
    end)
    UIDropDownMenu_SetWidth(detailChannelDropdown, 130)
    UIDropDownMenu_SetText(detailChannelDropdown, "Master")

    local triggerLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    triggerLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 175, -206)
    triggerLbl:SetText("시전 타입")

    detailTriggerDropdown = CreateFrame("Frame", "MSSTriggerDropdown", box3, "UIDropDownMenuTemplate")
    detailTriggerDropdown:SetPoint("TOPLEFT", box3, "TOPLEFT", 159, -224)

    UIDropDownMenu_Initialize(detailTriggerDropdown, function()
        local types = {
            { key = "instant", label = "즉시 시전 (Instant)" },
            { key = "cast",    label = "캐스팅 (Cast)"       },
            { key = "channel", label = "채널링 (Channel)"    },
        }
        for _, t in ipairs(types) do
            local info   = UIDropDownMenu_CreateInfo()
            info.text    = t.label
            info.value   = t.key
            info.checked = (selectedTriggerType == t.key)
            info.func    = function() SetTriggerType(t.key) end
            UIDropDownMenu_AddButton(info)
        end
    end)
    UIDropDownMenu_SetWidth(detailTriggerDropdown, 160)
    UIDropDownMenu_SetText(detailTriggerDropdown, TRIGGER_LABELS["instant"])

    local d3 = box3:CreateTexture(nil, "ARTWORK")
    d3:SetColorTexture(0.38, 0.38, 0.38, 0.5)
    d3:SetPoint("TOPLEFT",  box3, "TOPLEFT",  10, -262)
    d3:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -10, -262)
    d3:SetHeight(1)

    detailEnabledCheck = CreateFrame("CheckButton", "MSSEnabledCheck", box3, "UICheckButtonTemplate")
    detailEnabledCheck:SetPoint("TOPLEFT", box3, "TOPLEFT", 8, -270)
    detailEnabledCheck:SetChecked(true)

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

    local d4 = box3:CreateTexture(nil, "ARTWORK")
    d4:SetColorTexture(0.38, 0.38, 0.38, 0.5)
    d4:SetPoint("TOPLEFT",  box3, "TOPLEFT",  10, -310)
    d4:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -10, -310)
    d4:SetHeight(1)

    detailSaveBtn = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    detailSaveBtn:SetSize(100, 26)
    detailSaveBtn:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -322)
    detailSaveBtn:SetText("저장")
    detailSaveBtn:Disable()

    detailTestBtn = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    detailTestBtn:SetSize(110, 26)
    detailTestBtn:SetPoint("LEFT", detailSaveBtn, "RIGHT", 6, 0)
    detailTestBtn:SetText("테스트 재생")
    detailTestBtn:Disable() -- ★ 초기 비활성화

    detailDeleteBtn = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    detailDeleteBtn:SetSize(100, 26)
    detailDeleteBtn:SetPoint("LEFT", detailTestBtn, "RIGHT", 6, 0)
    detailDeleteBtn:SetText("삭제")
    detailDeleteBtn:Disable()

    detailStatusFS = box3:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailStatusFS:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -358)
    detailStatusFS:SetWidth(COL3_W - 20)
    detailStatusFS:SetJustifyH("LEFT")
    detailStatusFS:SetText("")

    -- ════════════════════════════════════════════════════════════
    --  이미지 플래시 섹션
    -- ════════════════════════════════════════════════════════════
    local imgDivider = box3:CreateTexture(nil, "ARTWORK")
    imgDivider:SetColorTexture(0.38, 0.38, 0.38, 0.5)
    imgDivider:SetPoint("TOPLEFT",  box3, "TOPLEFT",  10, -376)
    imgDivider:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -10, -376)
    imgDivider:SetHeight(1)

    local imgSectionLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    imgSectionLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -386)
    imgSectionLbl:SetText("|cffffcc00이미지 플래시|r")

    -- 활성화 체크
    detailImageEnabledCheck = CreateFrame("CheckButton", nil, box3, "UICheckButtonTemplate")
    detailImageEnabledCheck:SetSize(24, 24)
    detailImageEnabledCheck:SetPoint("TOPLEFT", box3, "TOPLEFT", 8, -406)
    detailImageEnabledCheck:SetChecked(false)
    local imgEnableLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    imgEnableLbl:SetPoint("LEFT", detailImageEnabledCheck, "RIGHT", 2, 0)
    imgEnableLbl:SetText("이미지 표시 활성화")

    -- 경로 입력
    local imgPathLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    imgPathLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -436)
    imgPathLbl:SetText("이미지 경로  (.tga / .png)")

    local imgPathHint = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    imgPathHint:SetPoint("LEFT", imgPathLbl, "RIGHT", 8, 0)
    imgPathHint:SetTextColor(0.5, 0.5, 0.5)
    imgPathHint:SetText("(예: Interface\\AddOns\\Moonlight_SkillSound\\image\\bear.tga)")

    detailImagePathEdit = CreateFrame("EditBox", nil, box3, "InputBoxTemplate")
    detailImagePathEdit:SetSize(COL3_W - 20, 22)
    detailImagePathEdit:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -454)
    detailImagePathEdit:SetAutoFocus(false)
    detailImagePathEdit:SetMaxLetters(512)

    -- 표시 시간 (초)
    local imgDurLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    imgDurLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -484)
    imgDurLbl:SetText("표시 시간 (초)")

    detailImageDurationDisplay = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    detailImageDurationDisplay:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -502)
    detailImageDurationDisplay:SetWidth(COL3_W - 20)
    detailImageDurationDisplay:SetJustifyH("CENTER")
    detailImageDurationDisplay:SetText("3.0")

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

    detailImageDurationPlus = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    detailImageDurationPlus:SetSize(32, 22)
    detailImageDurationPlus:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -10, -500)
    detailImageDurationPlus:SetText("+")
    detailImageDurationPlus:SetScript("OnClick", function()
        local cur = tonumber(detailImageDurationDisplay:GetText()) or 3.0
        local nxt = math.min(30.0, cur + 0.5)
        detailImageDurationDisplay:SetText(string.format("%.1f", nxt))
    end)

    -- 이미지 배율 (원본 기준 ×배) — 버튼 또는 직접 입력
    local imgScaleLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    imgScaleLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -524)
    imgScaleLbl:SetText("이미지 배율  (원본 기준 / 직접 입력 가능)")

    -- EditBox로 만들어 직접 숫자 입력 가능하게 함
    detailImageScaleDisplay = CreateFrame("EditBox", nil, box3, "InputBoxTemplate")
    detailImageScaleDisplay:SetSize(80, 22)
    detailImageScaleDisplay:SetPoint("CENTER", box3, "TOPLEFT", COL3_W / 2, -541)
    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 imgScaleMinus = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    imgScaleMinus:SetSize(36, 22)
    imgScaleMinus:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -540)
    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)

    local imgScalePlus = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    imgScalePlus:SetSize(36, 22)
    imgScalePlus:SetPoint("TOPRIGHT", box3, "TOPRIGHT", -10, -540)
    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)

    -- 이미지 위치 이동 버튼
    local imgPosLbl = box3:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    imgPosLbl:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -568)
    imgPosLbl:SetText("이미지 위치")

    detailImageMoveBtn = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    detailImageMoveBtn:SetSize(120, 24)
    detailImageMoveBtn:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -584)
    detailImageMoveBtn:SetText("이동 시작")

    local imgResetBtn = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    imgResetBtn:SetSize(80, 24)
    imgResetBtn:SetPoint("LEFT", detailImageMoveBtn, "RIGHT", 6, 0)
    imgResetBtn:SetText("초기화")
    imgResetBtn:SetScript("OnClick", 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)
    end)

    -- 이미지 테스트 버튼
    local imgTestBtn = CreateFrame("Button", nil, box3, "UIPanelButtonTemplate")
    imgTestBtn:SetSize(120, 24)
    imgTestBtn:SetPoint("TOPLEFT", box3, "TOPLEFT", 10, -614)
    imgTestBtn:SetText("▶ 이미지 미리보기")
    imgTestBtn:SetScript("OnClick", function()
        if not detailImagePathEdit then return end
        local path = detailImagePathEdit:GetText() or ""
        if path == "" then SetStatus("이미지 경로를 입력하세요.", 1, 0.3, 0.3); return end
        local dur = tonumber(detailImageDurationDisplay:GetText()) or 3.0
        local sc  = tonumber(detailImageScaleDisplay and detailImageScaleDisplay:GetText()) or 1.0
        -- 현재 프레임 위치 기준으로 임시 항목 생성
        imgFrame:SetSize(256, 256)
        imgFrame:SetScale(sc)
        imageMap[-1] = {
            path      = path,
            duration  = dur,
            scale     = sc,
            width     = 256,
            height    = 256,
            posX      = imgFrame:GetLeft()   or 0,
            posY      = imgFrame:GetBottom() or 0,
            posAnchor = "BOTTOMLEFT",
        }
        ShowSkillImage(-1)
        imageMap[-1] = nil
    end)

    -- 이동 버튼 스크립트
    local function UpdateImgMoveBtn()
        detailImageMoveBtn:SetText(imgMoveMode and "|cffff9999이동 중…|r" or "이동 시작")
    end

    detailImageMoveBtn:SetScript("OnClick", function()
        imgMoveMode = not imgMoveMode
        if imgMoveMode then
            -- 현재 경로/크기로 텍스처 보여주기
            local path = detailImagePathEdit:GetText() or ""
            if path ~= "" then imgTexture:SetTexture(path) end
            local sc = tonumber(detailImageScaleDisplay and 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()
            imgFrame:SetMovable(true); imgFrame:EnableMouse(true)
            imgFrame:RegisterForDrag("LeftButton")
            imgFrame:SetScript("OnDragStart", imgFrame.StartMoving)
            imgFrame:SetScript("OnDragStop", function(self)
                self:StopMovingOrSizing()
                if selectedClassID and selectedSpellID then
                    local db = MoonlightSkillSoundDB[selectedClassID]
                    if db and db[selectedSpellID] then
                        db[selectedSpellID].imagePosX = self:GetLeft()
                        db[selectedSpellID].imagePosY = self:GetBottom()
                        db[selectedSpellID].imagePosAnchor = "BOTTOMLEFT"
                    end
                end
            end)
        else
            imgFrame:SetMovable(false); imgFrame:EnableMouse(false)
            imgFrame:SetScript("OnDragStart", nil); imgFrame:SetScript("OnDragStop", nil)
            imgFrame:Hide(); imgTexture:SetAlpha(0)
        end
        UpdateImgMoveBtn()
    end)

    -------------------------------------------------------
    -- 버튼 스크립트
    -------------------------------------------------------
    detailSaveBtn:SetScript("OnClick", function()
        if not selectedClassID or not selectedSpellID then return end
        local path = detailPathEdit:GetText() or ""
        local db   = MoonlightSkillSoundDB[selectedClassID]
        if not db[selectedSpellID] then db[selectedSpellID] = {} end
        db[selectedSpellID].soundPath    = path
        db[selectedSpellID].soundChannel = selectedChannel
        db[selectedSpellID].enabled      = detailEnabledCheck:GetChecked()
        db[selectedSpellID].triggerType  = selectedTriggerType
        -- 이미지 필드 저장
        if detailImageEnabledCheck then
            db[selectedSpellID].imageEnabled  = detailImageEnabledCheck:GetChecked()
        end
        if detailImagePathEdit then
            db[selectedSpellID].imagePath = detailImagePathEdit:GetText() or ""
        end
        if detailImageDurationDisplay then
            db[selectedSpellID].imageDuration = tonumber(detailImageDurationDisplay:GetText()) or 3.0
        end
        if detailImageScaleDisplay then
            local sc = tonumber(detailImageScaleDisplay:GetText()) or 1.0
            db[selectedSpellID].imageScale = sc
            imgFrame:SetScale(sc)
        end
        -- 위치는 이동 버튼 DragStop에서 이미 저장됨
        RebuildRuntimeMap()
        RefreshSkillColumn(selectedClassID)
        SetStatus("저장되었습니다!", 0.3, 1, 0.3)
    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)

    -------------------------------------------------------
    -- 하단 버튼: 현재 직업 초기화
    -------------------------------------------------------
    local bottomDiv = mainFrame:CreateTexture(nil, "ARTWORK")
    bottomDiv:SetColorTexture(0.4, 0.4, 0.4, 0.5)
    bottomDiv:SetPoint("BOTTOMLEFT",  mainFrame, "BOTTOMLEFT",  16, 48)
    bottomDiv:SetPoint("BOTTOMRIGHT", mainFrame, "BOTTOMRIGHT", -16, 48)
    bottomDiv:SetHeight(1)

    local resetClassBtn = CreateFrame("Button", nil, mainFrame, "UIPanelButtonTemplate")
    resetClassBtn:SetSize(200, 28)
    resetClassBtn:SetPoint("BOTTOMLEFT", mainFrame, "BOTTOMLEFT", 16, 14)
    resetClassBtn:SetText("|cffffcc00|r 현재 직업 프리셋으로 초기화")
    resetClassBtn:SetScript("OnClick", 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)

    -------------------------------------------------------
    -- 하단 버튼: 모든 직업 초기화
    -------------------------------------------------------
    local resetAllBtn = CreateFrame("Button", nil, mainFrame, "UIPanelButtonTemplate")
    resetAllBtn:SetSize(200, 28)
    resetAllBtn:SetPoint("BOTTOMRIGHT", mainFrame, "BOTTOMRIGHT", -16, 14)
    resetAllBtn:SetText("|cffff6060|r 모든 직업 프리셋으로 초기화")
    resetAllBtn:SetScript("OnClick", 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)
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()

    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)
        end

    elseif event == "UNIT_SPELLCAST_START" then
        local unit, _, spellID = ...
        if unit == "player" and spellID then
            if not instantMap[spellID] then
                PlaySkillSound(spellID, "cast")
            end
            ShowSkillImage(spellID)
        end

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