local NS = rawget(_G, "MCDVoiceCooldownNS") or {}
_G.MCDVoiceCooldownNS = NS

NS.UI = NS.UI or {}
NS.UI.EditorFrameBuilder = NS.UI.EditorFrameBuilder or {}

local Builder = NS.UI.EditorFrameBuilder
local Widgets = NS.UI.Widgets
local Skin = NS.UI.Skin
local PopupLayout = NS.UI.PopupLayout
local EditorLayout = NS.UI.EditorLayout or {}
local EditorSoundFields = NS.UI.EditorSoundFields or {}
local EditorActions = NS.UI.EditorActions or {}
local L = NS.L or function(key, ...) if select("#", ...) > 0 then return string.format(tostring(key), ...) end return tostring(key) end

local CreateFieldLabel = EditorLayout.CreateFieldLabel
local PlaceControl = EditorLayout.PlaceControl
local PlaceModule = EditorLayout.PlaceModule
local CreateRateSlider = EditorLayout.CreateRateSlider
local InstallSpellIdAutofill = EditorLayout.InstallSpellIdAutofill
local InstallTalentIdAutofill = EditorLayout.InstallTalentIdAutofill

function Builder:EnsureFrame(owner)
    local self = owner
    if self.frame then
        return self.frame
    end

    local layout = PopupLayout and PopupLayout.Editor or nil
    local frameCfg = layout and layout.Frame or { width = 760, footerHeight = 54, contentTopY = -112, contentBottom = 66, sideInset = 24, tabY = -70, headerTitleY = -14 }

    local frame = CreateFrame("Frame", "MCDVoiceCooldownEditorFrame", UIParent, "BackdropTemplate")
    frame:SetSize(frameCfg.width or 760, 720)
    frame:SetPoint("CENTER", UIParent, "CENTER", 100, 0)
    frame:SetFrameStrata("FULLSCREEN_DIALOG")
    frame:SetFrameLevel(100)
    frame:SetToplevel(true)
    frame:EnableMouse(true)
    frame:SetMovable(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
    frame:SetClampedToScreen(true)

    local function UpdateFrameHeight()
        if PopupLayout and PopupLayout.UpdatePopupHeight then
            PopupLayout:UpdatePopupHeight(frame)
            return
        end
        local parentHeight = UIParent and UIParent.GetHeight and (UIParent:GetHeight() or 720) or 720
        local targetHeight = math.min(780, math.max(620, parentHeight - 80))
        frame:SetHeight(targetHeight)
        if frame.contentHost and frame.contentHost.UpdateScrollRange then
            frame.contentHost:UpdateScrollRange()
        end
    end

    frame:SetScript("OnShow", function(selfFrame)
        UpdateFrameHeight()
        selfFrame:SetFrameLevel(100)
        selfFrame:Raise()
    end)
    frame:Hide()
    Widgets:ApplyPanelChrome(frame, { footerHeight = frameCfg.footerHeight or 54 })
    tinsert(UISpecialFrames, frame:GetName())

    local title = Widgets:CreateLabel(frame, L("TITLE_EDIT_CONFIG"), "GameFontNormalLarge")
    title:SetPoint("TOP", frame, "TOP", 0, frameCfg.headerTitleY or -14)
    title:SetWidth(560)
    title:SetJustifyH("CENTER")
    if Skin then
        Skin:StyleFont(title, "title")
    end
    frame.title = title

    local description = PopupLayout and PopupLayout.CreateHeaderDescription and PopupLayout:CreateHeaderDescription(frame, L("EDITOR_DESC")) or Widgets:CreateLabel(frame, L("EDITOR_DESC"), "GameFontHighlightSmall")
    if description and not description:GetPoint() then
        description:SetPoint("TOP", title, "BOTTOM", 0, -4)
    end
    frame.description = description

    local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    close:SetSize(28, 28)
    close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -10, -10)
    if Skin and Skin.SkinCloseButton then
        Skin:SkinCloseButton(close)
    end
    close:SetScript("OnClick", function()
        self:Close()
    end)

    local tabCooldown = Widgets:CreateButton(frame, L("TAB_COOLDOWN"), 112, 32)
    tabCooldown:SetPoint("TOPLEFT", frame, "TOPLEFT", 24, frameCfg.tabY or -70)

    local tabCast = Widgets:CreateButton(frame, L("TAB_CAST"), 120, 32)
    tabCast:SetPoint("LEFT", tabCooldown, "RIGHT", 8, 0)

    local tabBloodlust = Widgets:CreateButton(frame, L("TAB_BLOODLUST"), 120, 32)
    tabBloodlust:SetPoint("LEFT", tabCast, "RIGHT", 8, 0)

    local contentHost, content = Widgets:CreateScrollableContent(frame, { contentHeight = (PopupLayout and PopupLayout.Editor and PopupLayout.Editor.Rows.contentHeight) or 720, contentWidth = 700, topHeight = 16, noTopLine = true })
    contentHost:SetPoint("TOPLEFT", frame, "TOPLEFT", frameCfg.sideInset or 24, frameCfg.contentTopY or -112)
    contentHost:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -(frameCfg.sideInset or 24), frameCfg.contentBottom or 66)
    contentHost:SetContentHeight((PopupLayout and PopupLayout.Editor and PopupLayout.Editor.Rows.contentHeight) or 720)
    frame.contentHost = contentHost

    -- 弹窗布局只从 PopupLayout 取常量；后续新增字段时优先改 UI/PopupLayout.lua。
    local grid = (PopupLayout and PopupLayout.Editor and PopupLayout.Editor.Grid) or {}
    local modules = (PopupLayout and PopupLayout.Editor and PopupLayout.Editor.Modules) or {}

    -- Unified module layout: all controls inside a module are parented to that
    -- module card and use local coordinates. This prevents controls from
    -- visually escaping the card when the popup is scrolled or resized.
    local INNER = grid.moduleInnerLeft or 16
    local MODULE_W = modules.width or 680
    local MODULE_RIGHT = grid.moduleInnerRight or 16
    local MODULE_CONTENT_W = MODULE_W - INNER - MODULE_RIGHT

    -- Unified module-local grid. All controls in each module use these local
    -- X/Y positions instead of ad-hoc popup-global coordinates.
    local LABEL_OFFSET = 0
    local CONTROL_OFFSET = -24
    local ROW_GAP = 76
    local ID_X, ID_W = INNER, grid.idW or 112
    local NAME_X, NAME_W = 152, grid.nameW or 220
    local CD_X, CD_W = 392, grid.cdW or 112
    local CHECK_X, CHECK_W = 520, grid.checkW or 126
    local CLASS_LABEL_X, DROP_CLASS_X, DROP_CLASS_W = INNER, INNER, grid.classDropW or 286
    local SPEC_LABEL_X, DROP_SPEC_X, DROP_SPEC_W = 342, 342, grid.specDropW or 286
    local SOURCE_X, SOURCE_W = INNER, grid.sourceW or 286
    local BUILTIN_X, BUILTIN_W = INNER, grid.halfW or 286
    local SHAREDMEDIA_X, SHAREDMEDIA_W = 342, grid.halfW or 286
    local CUSTOM_X, CUSTOM_W = INNER, MODULE_CONTENT_W
    local TTS_X, TTS_W = INNER, grid.halfW or 286
    local SLIDER_LABEL_X, SLIDER_X = SHAREDMEDIA_X, SHAREDMEDIA_X
    local BLOODLUST_W = MODULE_CONTENT_W

    local CLASS_LABEL_Y, CLASS_CONTROL_Y = -38, -62
    local SPELL_ROW1_Y = -46
    local SPELL_ROW2_Y = SPELL_ROW1_Y - ROW_GAP
    local SPELL_LABEL1_Y, SPELL_CONTROL1_Y = SPELL_ROW1_Y + LABEL_OFFSET, SPELL_ROW1_Y + CONTROL_OFFSET
    local SPELL_LABEL2_Y, SPELL_CONTROL2_Y = SPELL_ROW2_Y + LABEL_OFFSET, SPELL_ROW2_Y + CONTROL_OFFSET
    local NOTIFY_ROW1_Y = -46
    local NOTIFY_ROW2_Y = NOTIFY_ROW1_Y - ROW_GAP
    local NOTIFY_ROW3_Y = NOTIFY_ROW2_Y - ROW_GAP
    local NOTIFY_ROW4_Y = NOTIFY_ROW3_Y - ROW_GAP
    local NOTIFY_SOURCE_LABEL_Y, NOTIFY_SOURCE_CONTROL_Y = NOTIFY_ROW1_Y + LABEL_OFFSET, NOTIFY_ROW1_Y + CONTROL_OFFSET
    local NOTIFY_SOUND_LABEL_Y, NOTIFY_SOUND_CONTROL_Y = NOTIFY_ROW2_Y + LABEL_OFFSET, NOTIFY_ROW2_Y + CONTROL_OFFSET
    local NOTIFY_CUSTOM_LABEL_Y, NOTIFY_CUSTOM_CONTROL_Y = NOTIFY_ROW3_Y + LABEL_OFFSET, NOTIFY_ROW3_Y + CONTROL_OFFSET
    local NOTIFY_TTS_LABEL_Y, NOTIFY_TTS_CONTROL_Y = NOTIFY_ROW4_Y + LABEL_OFFSET, NOTIFY_ROW4_Y + CONTROL_OFFSET
    local NOTIFY_HINT_Y = NOTIFY_TTS_CONTROL_Y - 46
    local actionSave = Widgets:CreateButton(frame, L("BTN_SAVE"), 120, 32)
    actionSave:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 24, 16)

    local actionTest = Widgets:CreateButton(frame, L("BTN_TEST"), 120, 32)
    actionTest:SetPoint("LEFT", actionSave, "RIGHT", 8, 0)

    -- 普通配置页：职业 / 专精
    local classSection = PlaceModule(content, L("SECTION_CLASS_SPEC"), modules.classTop or -8, modules.classHeight or 86)
    local classLabel = CreateFieldLabel(classSection, L("LABEL_CLASS"), CLASS_LABEL_X, CLASS_LABEL_Y, DROP_CLASS_W)
    local specLabel = CreateFieldLabel(classSection, L("LABEL_SPEC"), SPEC_LABEL_X, CLASS_LABEL_Y, DROP_SPEC_W)

    local classDrop = PlaceControl(Widgets:CreateDropdown(classSection, "MCDVoiceCooldownEditorClassDropDown", DROP_CLASS_W), classSection, DROP_CLASS_X, CLASS_CONTROL_Y)
    local specDrop = PlaceControl(Widgets:CreateDropdown(classSection, "MCDVoiceCooldownEditorSpecDropDown", DROP_SPEC_W), classSection, DROP_SPEC_X, CLASS_CONTROL_Y)

    -- 普通配置页：技能参数
    local spellSection = PlaceModule(content, L("SECTION_SPELL_PARAMS"), modules.spellTop or -102, modules.spellHeight or 178)
    local spellIdLabel = CreateFieldLabel(spellSection, L("LABEL_SPELL_ID"), ID_X, SPELL_LABEL1_Y, ID_W)
    local spellNameLabel = CreateFieldLabel(spellSection, L("LABEL_SPELL_NAME"), NAME_X, SPELL_LABEL1_Y, NAME_W)
    local baseCDLabel = CreateFieldLabel(spellSection, L("LABEL_FIXED_CD_SEC"), CD_X, SPELL_LABEL1_Y, CD_W)

    local spellId = PlaceControl(Widgets:CreateEditBox(spellSection, ID_W, 30, true), spellSection, ID_X, SPELL_CONTROL1_Y)
    local spellName = PlaceControl(Widgets:CreateEditBox(spellSection, NAME_W, 30, false), spellSection, NAME_X, SPELL_CONTROL1_Y)
    local baseCD = PlaceControl(Widgets:CreateEditBox(spellSection, CD_W, 30, false), spellSection, CD_X, SPELL_CONTROL1_Y)

    local checkTalent = Widgets:CreateCheckButton(spellSection, L("LABEL_CHECK_TALENT"), CHECK_W - 30)
    checkTalent:SetPoint("TOPLEFT", spellSection, "TOPLEFT", CHECK_X, SPELL_CONTROL1_Y - 3)
    checkTalent.mcdMaxWidth = CHECK_W
    if checkTalent.mcdLabel and checkTalent.mcdLabel.SetWidth then
        checkTalent.mcdLabel:SetWidth(math.max(1, CHECK_W - 30))
    end

    -- CD提示专用：与上方三列严格对齐；未勾选“检查天赋”时全部禁用变灰。
    local talentIdLabel = CreateFieldLabel(spellSection, L("LABEL_TALENT_ID"), ID_X, SPELL_LABEL2_Y, ID_W)
    local talentNameLabel = CreateFieldLabel(spellSection, L("LABEL_TALENT_NAME"), NAME_X, SPELL_LABEL2_Y, NAME_W)
    local talentCDLabel = CreateFieldLabel(spellSection, L("LABEL_TALENT_CD_SEC"), CD_X, SPELL_LABEL2_Y, CD_W)

    local talentId = PlaceControl(Widgets:CreateEditBox(spellSection, ID_W, 30, true), spellSection, ID_X, SPELL_CONTROL2_Y)
    local talentName = PlaceControl(Widgets:CreateEditBox(spellSection, NAME_W, 30, false), spellSection, NAME_X, SPELL_CONTROL2_Y)
    local talentCD = PlaceControl(Widgets:CreateEditBox(spellSection, CD_W, 30, false), spellSection, CD_X, SPELL_CONTROL2_Y)

    -- 施法成功专用：可选延时播放。与“检查天赋”所在区域复用空间，两个标签页互斥显示。
    local delayEnabled = Widgets:CreateCheckButton(spellSection, L("LABEL_DELAY_CAST_SUCCESS"), 220)
    delayEnabled:SetPoint("TOPLEFT", spellSection, "TOPLEFT", ID_X, SPELL_CONTROL2_Y - 3)
    delayEnabled.mcdMaxWidth = 220
    if delayEnabled.mcdLabel and delayEnabled.mcdLabel.SetWidth then
        delayEnabled.mcdLabel:SetWidth(190)
    end

    local delaySecondsLabel = CreateFieldLabel(spellSection, L("LABEL_DELAY_SECONDS"), CD_X, SPELL_LABEL2_Y, CD_W)
    local delaySeconds = PlaceControl(Widgets:CreateEditBox(spellSection, CD_W, 30, false), spellSection, CD_X, SPELL_CONTROL2_Y)

    -- 普通配置页：通知方式
    local notifySection = PlaceModule(content, L("SECTION_NOTIFY"), modules.notifyTop or -294, modules.notifyHeight or 382)
    local sourceLabel = CreateFieldLabel(notifySection, L("LABEL_SOUND_SOURCE"), SOURCE_X, NOTIFY_SOURCE_LABEL_Y, SOURCE_W)

    local sourceDrop = PlaceControl(Widgets:CreateDropdown(notifySection, "MCDVoiceCooldownEditorSoundSourceDropDown", SOURCE_W), notifySection, SOURCE_X, NOTIFY_SOURCE_CONTROL_Y)

    local builtinLabel = CreateFieldLabel(notifySection, L("LABEL_BUILTIN_SOUND"), BUILTIN_X, NOTIFY_SOUND_LABEL_Y, BUILTIN_W)

    local builtinDrop = PlaceControl(Widgets:CreateDropdown(notifySection, "MCDVoiceCooldownEditorBuiltinDropDown", BUILTIN_W), notifySection, BUILTIN_X, NOTIFY_SOUND_CONTROL_Y)

    local sharedMediaLabel = CreateFieldLabel(notifySection, L("LABEL_SHAREDMEDIA_SOUND"), SHAREDMEDIA_X, NOTIFY_SOUND_LABEL_Y, SHAREDMEDIA_W)

    local sharedMediaDrop = PlaceControl(Widgets:CreateDropdown(notifySection, "MCDVoiceCooldownEditorSharedMediaDropDown", SHAREDMEDIA_W), notifySection, SHAREDMEDIA_X, NOTIFY_SOUND_CONTROL_Y)

    local customPathLabel = CreateFieldLabel(notifySection, L("LABEL_CUSTOM_SOUND_PATH"), CUSTOM_X, NOTIFY_CUSTOM_LABEL_Y, CUSTOM_W)

    local soundPath = PlaceControl(Widgets:CreateEditBox(notifySection, CUSTOM_W, 30, false), notifySection, CUSTOM_X, NOTIFY_CUSTOM_CONTROL_Y)

    local ttsTextLabel = CreateFieldLabel(notifySection, L("LABEL_TTS_TEXT"), TTS_X, NOTIFY_TTS_LABEL_Y, TTS_W)

    local ttsText = PlaceControl(Widgets:CreateEditBox(notifySection, TTS_W, 30, false), notifySection, TTS_X, NOTIFY_TTS_CONTROL_Y)

    local rateLabel = CreateFieldLabel(notifySection, L("LABEL_TTS_RATE"), SLIDER_LABEL_X, NOTIFY_TTS_LABEL_Y, TTS_W)
    local ttsRateSlider = CreateRateSlider(notifySection)
    -- 30px 高度的原生滑条与左侧 TTS 输入框同一行、同一垂直中心。
    PlaceControl(ttsRateSlider, notifySection, SLIDER_X, NOTIFY_TTS_CONTROL_Y)

    local ttsRateValue = Widgets:CreateLabel(notifySection, "0", "GameFontHighlightSmall")
    ttsRateValue:SetPoint("LEFT", ttsRateSlider, "RIGHT", 8, 0)
    ttsRateValue:SetWidth(42)
    ttsRateValue:SetJustifyH("LEFT")
    ttsRateValue:SetJustifyV("MIDDLE")
    if Skin then
        Skin:StyleFont(ttsRateValue, "body")
    end
    ttsRateSlider.mcdValueText = ttsRateValue

    local editorHint = Widgets:CreateLabel(notifySection, L("EDITOR_HINT"), "GameFontHighlightSmall")
    editorHint:SetPoint("TOPLEFT", notifySection, "TOPLEFT", INNER, NOTIFY_HINT_Y)
    editorHint:SetPoint("RIGHT", notifySection, "RIGHT", -MODULE_RIGHT, 0)
    editorHint:SetJustifyH("LEFT")
    if Skin then
        Skin:StyleFont(editorHint, "muted")
    end

    -- 嗜血配置页：单独布局，避免大字体时文本和输入框重叠
    local bloodlustSection = PlaceModule(content, L("SECTION_BLOODLUST_SOUND"), modules.bloodlustTop or -10, modules.bloodlustHeight or 474)
    local bloodlustSummary = Widgets:CreateLabel(bloodlustSection, "", "GameFontHighlightSmall")
    bloodlustSummary:SetPoint("TOPLEFT", bloodlustSection, "TOPLEFT", INNER, -44)
    bloodlustSummary:SetPoint("RIGHT", bloodlustSection, "RIGHT", -MODULE_RIGHT, 0)
    bloodlustSummary:SetJustifyH("LEFT")
    if Skin then
        Skin:StyleFont(bloodlustSummary, "muted")
    end

    local bloodlustLabels = {}
    local bloodlustFields = {}
    for i = 1, 5 do
        local y = -82 - ((i - 1) * 62)
        local label = CreateFieldLabel(bloodlustSection, L("LABEL_SOUND_PATH_N", i), INNER, y, BLOODLUST_W)
        local box = PlaceControl(Widgets:CreateEditBox(bloodlustSection, BLOODLUST_W, 30, false), bloodlustSection, INNER, y - 26)
        bloodlustLabels[i] = label
        bloodlustFields[i] = box
    end

    local bloodlustHint = Widgets:CreateLabel(bloodlustSection, L("BLOODLUST_HINT"), "GameFontHighlightSmall")
    bloodlustHint:SetPoint("TOPLEFT", bloodlustSection, "TOPLEFT", INNER, -430)
    bloodlustHint:SetPoint("RIGHT", bloodlustSection, "RIGHT", -MODULE_RIGHT, 0)
    bloodlustHint:SetJustifyH("LEFT")
    if Skin then
        Skin:StyleFont(bloodlustHint, "muted")
    end


    -- Development guard: keep the layout contract visible in code.  Every field
    -- created above is parented to its module card, not directly to the scroll
    -- content.  If a future edit puts a widget outside its module, fix the
    -- module-local X/Y values in PopupLayout/this grid instead of relying on
    -- clipping.
    frame.widgets = {
        tabCooldown = tabCooldown,
        tabCast = tabCast,
        tabBloodlust = tabBloodlust,
        classSection = classSection,
        spellSection = spellSection,
        notifySection = notifySection,
        classLabel = classLabel,
        specLabel = specLabel,
        spellIdLabel = spellIdLabel,
        spellNameLabel = spellNameLabel,
        talentIdLabel = talentIdLabel,
        talentNameLabel = talentNameLabel,
        talentCDLabel = talentCDLabel,
        classDrop = classDrop,
        specDrop = specDrop,
        spellId = spellId,
        spellName = spellName,
        checkTalent = checkTalent,
        talentId = talentId,
        talentName = talentName,
        talentCDLabel = talentCDLabel,
        talentCD = talentCD,
        delayEnabled = delayEnabled,
        delaySecondsLabel = delaySecondsLabel,
        delaySeconds = delaySeconds,
        baseCDLabel = baseCDLabel,
        baseCD = baseCD,
        sourceLabel = sourceLabel,
        sourceDrop = sourceDrop,
        builtinLabel = builtinLabel,
        builtinDrop = builtinDrop,
        sharedMediaLabel = sharedMediaLabel,
        sharedMediaDrop = sharedMediaDrop,
        customPathLabel = customPathLabel,
        ttsTextLabel = ttsTextLabel,
        rateLabel = rateLabel,
        ttsText = ttsText,
        ttsRateSlider = ttsRateSlider,
        ttsRateValue = ttsRateValue,
        soundPath = soundPath,
        actionSave = actionSave,
        actionTest = actionTest,
        editorHint = editorHint,
        bloodlustSection = bloodlustSection,
        bloodlustSummary = bloodlustSummary,
        bloodlustLabels = bloodlustLabels,
        bloodlustFields = bloodlustFields,
        bloodlustHint = bloodlustHint,
    }

    InstallSpellIdAutofill(self, spellId)
    InstallTalentIdAutofill(self, talentId)
    if EditorSoundFields and type(EditorSoundFields.InstallHandlers) == "function" then
        EditorSoundFields:InstallHandlers(self, frame)
    end

    if EditorActions and type(EditorActions.Install) == "function" then
        EditorActions:Install(self, frame)
    end

    self.frame = frame
    return frame
end
