src/GUI/Layout.ps1

# 按 schema 动态生成窗体与文件对话框。New-GUIForm 只构建、不显示,便于无头测试。

# 按点号路径在嵌套哈希表里取字符串叶子;缺键、中途不是表或叶子不是字符串时返回 $null。
function Resolve-NestedText {
    param($Data, [string]$Key)
    $current = $Data
    foreach ($part in ($Key -split '\.')) {
        if ($current -isnot [System.Collections.IDictionary] -or -not $current.Contains($part)) { return $null }
        $current = $current[$part]
    }
    if ($current -is [string]) { return $current }
    return $null
}

# GUI 文案三级回退:当前语言 GUI → en-US GUI → 键名本身。$Script:LocalizeData 缺失时也不崩。
function Get-GUIText {
    param([string]$Key)
    if ($Script:LocalizeData) {
        $text = Resolve-NestedText $Script:LocalizeData.GUI $Key
        if ($null -ne $text) { return $text }
    }
    if ($null -eq $Script:GUIFallback) {
        $Script:GUIFallback = @{}
        try {
            $en = & "$PSScriptRoot/../locale/en-US.ps1"
            if ($en -and $en.GUI) { $Script:GUIFallback = $en.GUI }
        }
        catch { }
    }
    $fallback = Resolve-NestedText $Script:GUIFallback $Key
    if ($null -ne $fallback) { return $fallback }
    return $Key
}

# 沿 ConsoleHelpData.PrarmsData 的点号路径取帮助文本,去掉 markdown 反引号。
function Get-ParamHelp {
    param([string]$Path)
    if (-not $Path -or -not $Script:LocalizeData) { return '' }
    $help = Resolve-NestedText $Script:LocalizeData.ConsoleHelpData.PrarmsData $Path
    if ($null -eq $help) { return '' }
    return ($help -replace '`', '').Trim()
}

function New-GUIRowStyle {
    return New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::AutoSize)
}

function New-GUIPercentRowStyle {
    param([float]$Percent = 100)
    return New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, $Percent)
}

function New-GUIButton {
    param([string]$Text)
    $button = New-Object System.Windows.Forms.Button
    $button.Text = $Text
    $button.AutoSize = $true
    $button.AutoSizeMode = 'GrowAndShrink'
    $button.Padding = New-Object System.Windows.Forms.Padding(12, 0, 12, 0)
    $button.Margin = New-Object System.Windows.Forms.Padding(4, 0, 0, 0)
    $button.MinimumSize = New-Object System.Drawing.Size(0, 25)
    return $button
}

# 跨两列的字段组标题(Flags/DllExports 这类整块控件用)。
function New-GUISectionLabel {
    param([string]$Text, [System.Windows.Forms.TableLayoutPanel]$Grid, [int]$Row)
    $label = New-Object System.Windows.Forms.Label
    $label.Text = $Text
    $label.AutoSize = $true
    $label.Anchor = [System.Windows.Forms.AnchorStyles]::Left
    $label.TextAlign = 'MiddleLeft'
    $label.Margin = New-Object System.Windows.Forms.Padding(4, 4, 4, 0)
    $Grid.Controls.Add($label, 0, $Row)
    $Grid.SetColumnSpan($label, 2)
    return $label
}

function New-GUIOptionFlow {
    $flow = New-Object System.Windows.Forms.FlowLayoutPanel
    $flow.AutoSize = $true
    $flow.Dock = 'Top'
    $flow.FlowDirection = 'LeftToRight'
    $flow.Margin = New-Object System.Windows.Forms.Padding(2, 0, 4, 4)
    return $flow
}

# 生成一个字段对应的控件;返回占用的表格行数。
function New-GUIFieldRow {
    param(
        $Field,
        [System.Windows.Forms.TableLayoutPanel]$Grid,
        [int]$Row
    )
    $path = $Field.Path
    $labelKey = if ($Field.Label) { $Field.Label } else { "Field.$path" }
    $labelText = Get-GUIText $labelKey
    $help = if ($Field.Help) { Get-GUIText $Field.Help } else { Get-ParamHelp $path }
    $toolTip = $Script:GUIToolTip

    switch ($Field.Kind) {
        'Bool' {
            $box = New-Object System.Windows.Forms.CheckBox
            $box.Text = $labelText
            $box.AutoSize = $true
            $box.Checked = [bool]$Field.Default
            $box.Margin = New-Object System.Windows.Forms.Padding(4, 5, 4, 5)
            $Grid.Controls.Add($box, 0, $Row)
            $Grid.SetColumnSpan($box, 2)
            [void]$Grid.RowStyles.Add((New-GUIRowStyle))
            $toolTip.SetToolTip($box, $help)
            $Script:FieldControls[$path] = $box
            return 1
        }
        'Flags' {
            $label = New-GUISectionLabel $labelText $Grid $Row
            $flow = New-GUIOptionFlow
            $selected = @($Field.Default | ForEach-Object { [string]$_ })
            foreach ($option in $Field.Options) {
                $box = New-Object System.Windows.Forms.CheckBox
                $box.Text = [string]$option
                $box.AutoSize = $true
                $box.Checked = $selected -contains [string]$option
                $box.Margin = New-Object System.Windows.Forms.Padding(2, 2, 8, 2)
                $flow.Controls.Add($box)
            }
            $Grid.Controls.Add($flow, 0, $Row + 1)
            $Grid.SetColumnSpan($flow, 2)
            [void]$Grid.RowStyles.Add((New-GUIRowStyle))
            [void]$Grid.RowStyles.Add((New-GUIRowStyle))
            $toolTip.SetToolTip($label, $help)
            $toolTip.SetToolTip($flow, $help)
            $Script:FieldControls[$path] = $flow
            return 2
        }
        'DllExports' {
            $label = New-GUISectionLabel $labelText $Grid $Row
            $flow = New-GUIOptionFlow
            $addButton = New-GUIButton (Get-GUIText 'Button.AddExport')
            $removeButton = New-GUIButton (Get-GUIText 'Button.RemoveExport')
            $helpLabel = New-Object System.Windows.Forms.Label
            $helpLabel.Text = $help
            $helpLabel.AutoSize = $true
            $helpLabel.Anchor = [System.Windows.Forms.AnchorStyles]::Left
            $helpLabel.TextAlign = 'MiddleLeft'
            $helpLabel.Margin = New-Object System.Windows.Forms.Padding(8, 6, 4, 0)
            $flow.Controls.Add($addButton)
            $flow.Controls.Add($removeButton)
            $flow.Controls.Add($helpLabel)
            $Grid.Controls.Add($flow, 0, $Row + 1)
            $Grid.SetColumnSpan($flow, 2)

            $gridView = New-Object System.Windows.Forms.DataGridView
            $gridView.AutoSizeColumnsMode = 'Fill'
            $gridView.AllowUserToAddRows = $true
            $gridView.AllowUserToResizeRows = $false
            $gridView.Dock = 'Fill'
            $gridView.Margin = New-Object System.Windows.Forms.Padding(4, 0, 4, 4)
            $gridView.RowHeadersVisible = $false
            $gridView.SelectionMode = 'FullRowSelect'
            $gridView.ColumnHeadersHeightSizeMode = 'DisableResizing'
            $gridView.ColumnHeadersHeight = 30
            $gridView.RowTemplate.Height = 26
            $gridView.BorderStyle = 'FixedSingle'
            $gridView.MinimumSize = New-Object System.Drawing.Size(0, 130)
            foreach ($column in @(
                    @{ Name = 'FuncName'; Header = 'Field.Build.DllExports.FuncName' }
                    @{ Name = 'ReturnType'; Header = 'Field.Build.DllExports.ReturnType' }
                    @{ Name = 'Params'; Header = 'Field.Build.DllExports.Params' }
                )) {
                $columnControl = New-Object System.Windows.Forms.DataGridViewTextBoxColumn
                $columnControl.Name = $column.Name
                $columnControl.HeaderText = Get-GUIText $column.Header
                [void]$gridView.Columns.Add($columnControl)
            }
            $Grid.Controls.Add($gridView, 0, $Row + 2)
            $Grid.SetColumnSpan($gridView, 2)
            [void]$Grid.RowStyles.Add((New-GUIRowStyle))
            [void]$Grid.RowStyles.Add((New-GUIRowStyle))
            [void]$Grid.RowStyles.Add((New-GUIRowStyle))
            $toolTip.SetToolTip($label, $help)
            $toolTip.SetToolTip($gridView, $help)
            $Script:refs.AddExportButton = $addButton
            $Script:refs.RemoveExportButton = $removeButton
            $Script:FieldControls[$path] = $gridView
            return 3
        }
        default {
            $label = New-Object System.Windows.Forms.Label
            $label.Text = $labelText
            $label.AutoSize = $true
            $label.Anchor = [System.Windows.Forms.AnchorStyles]::Left
            $label.TextAlign = 'MiddleLeft'
            $label.Margin = New-Object System.Windows.Forms.Padding(4, 6, 12, 6)
            $Grid.Controls.Add($label, 0, $Row)

            $control = $null
            $browseButton = $null
            switch ($Field.Kind) {
                'Choice' {
                    $control = New-Object System.Windows.Forms.ComboBox
                    $control.DropDownStyle = 'DropDownList'
                    foreach ($choice in $Field.Choices) { [void]$control.Items.Add([string]$choice) }
                    $default = [string]$Field.Default
                    if (@($control.Items | ForEach-Object { [string]$_ }) -contains $default) { $control.SelectedItem = $default }
                    elseif ($control.Items.Count -gt 0) { $control.SelectedIndex = 0 }
                }
                'ChoiceEdit' {
                    $control = New-Object System.Windows.Forms.ComboBox
                    $control.DropDownStyle = 'DropDown'
                    foreach ($choice in $Field.Choices) { [void]$control.Items.Add([string]$choice) }
                    $control.Text = [string]$Field.Default
                }
                'Password' {
                    $control = New-Object System.Windows.Forms.TextBox
                    $control.PasswordChar = '*'
                    $control.Text = [string]$Field.Default
                }
                'Script' {
                    $control = New-Object System.Windows.Forms.TextBox
                    $control.Multiline = $true
                    $control.AcceptsReturn = $true
                    $control.ScrollBars = 'Vertical'
                    $rows = if ($Field.Rows) { [int]$Field.Rows } else { 3 }
                    $lineHeight = [int](($control.Font.Height * $rows) + 8)
                    $control.MinimumSize = New-Object System.Drawing.Size -ArgumentList 0, $lineHeight
                    $control.Text = [string]$Field.Default
                }
                'Path' {
                    $pathPanel = New-Object System.Windows.Forms.TableLayoutPanel
                    $pathPanel.Dock = 'Fill'
                    $pathPanel.AutoSize = $true
                    $pathPanel.AutoSizeMode = 'GrowAndShrink'
                    $pathPanel.ColumnCount = 2
                    $pathPanel.RowCount = 1
                    [void]$pathPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100)))
                    [void]$pathPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
                    [void]$pathPanel.RowStyles.Add((New-GUIRowStyle))

                    $control = New-Object System.Windows.Forms.TextBox
                    $control.Dock = 'Fill'
                    $control.AllowDrop = $true
                    $control.Margin = New-Object System.Windows.Forms.Padding(0, 0, 4, 0)
                    $control.Text = [string]$Field.Default

                    $browseButton = New-GUIButton (Get-GUIText 'Button.Browse')
                    $browseButton.Margin = New-Object System.Windows.Forms.Padding(0)
                    if ($browseButton.Text -eq 'Button.Browse') { $browseButton.Text = '...' }

                    $pathPanel.Controls.Add($control, 0, 0)
                    $pathPanel.Controls.Add($browseButton, 1, 0)
                    $textControl = $control
                    $inputControl = $pathPanel
                }
                default {
                    $control = New-Object System.Windows.Forms.TextBox
                    $control.Text = [string]$Field.Default
                }
            }

            if (-not $inputControl) { $inputControl = $control; $textControl = $control }
            $inputControl.Dock = 'Fill'
            $inputControl.Margin = New-Object System.Windows.Forms.Padding(0, 4, 4, 4)
            $Grid.Controls.Add($inputControl, 1, $Row)
            [void]$Grid.RowStyles.Add((New-GUIRowStyle))
            if ($browseButton) { $Script:FieldBrowse[$path] = $browseButton }

            $toolTip.SetToolTip($label, $help)
            $toolTip.SetToolTip($textControl, $help)
            if ($browseButton) { $toolTip.SetToolTip($browseButton, $help) }
            $Script:FieldControls[$path] = $textControl
            return 1
        }
    }
}

function New-GUIForm {
    $Script:refs = @{}
    $Script:FieldControls = @{}
    $Script:FieldBrowse = @{}
    $Script:GUIToolTip = New-Object System.Windows.Forms.ToolTip

    $form = New-Object System.Windows.Forms.Form
    $form.Text = Get-GUIText 'Window.Title'
    $form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Dpi
    $form.ClientSize = New-Object System.Drawing.Size(1040, 780)
    $form.MinimumSize = New-Object System.Drawing.Size(900, 660)
    try { $form.Font = New-Object System.Drawing.Font('Segoe UI', 9) } catch { }
    $Script:refs.MainForm = $form

    $root = New-Object System.Windows.Forms.TableLayoutPanel
    $root.Dock = 'Fill'
    $root.Padding = New-Object System.Windows.Forms.Padding(10, 8, 10, 10)
    $root.ColumnCount = 1
    $root.RowCount = 3
    [void]$root.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100)))
    [void]$root.RowStyles.Add((New-GUIPercentRowStyle 100))
    [void]$root.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 130)))
    [void]$root.RowStyles.Add((New-GUIRowStyle))
    $form.Controls.Add($root)

    $tabs = New-Object System.Windows.Forms.TabControl
    $tabs.Dock = 'Fill'
    $root.Controls.Add($tabs, 0, 0)
    $Script:refs.TabsControl = $tabs

    foreach ($page in $Script:GUISchema.Pages) {
        $tabPage = New-Object System.Windows.Forms.TabPage
        $tabPage.Text = Get-GUIText $page.Label

        # TableLayoutPanel 的 AutoScroll 在 Dock=Fill + Percent 行的组合下不会为 AutoSize 分组撑出滚动条,
        # 导致靠下的分组(如 DLL 导出)被裁掉且无法滚动到。改为外层 Panel 负责滚动、内层 AutoSize 表格随内容长高。
        $pageScroll = New-Object System.Windows.Forms.Panel
        $pageScroll.Dock = 'Fill'
        $pageScroll.AutoScroll = $true
        $tabPage.Controls.Add($pageScroll)

        $pageGrid = New-Object System.Windows.Forms.TableLayoutPanel
        $pageGrid.Dock = 'Top'
        $pageGrid.AutoSize = $true
        $pageGrid.AutoSizeMode = 'GrowAndShrink'
        $pageGrid.Padding = New-Object System.Windows.Forms.Padding(8)
        $pageGrid.ColumnCount = 1
        [void]$pageGrid.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100)))
        $pageScroll.Controls.Add($pageGrid)
        $tabs.TabPages.Add($tabPage)

        $groupRow = 0
        foreach ($group in $page.Groups) {
            $groupBox = New-Object ps12exeGUI.FlatGroupBox
            $groupBox.Text = Get-GUIText $group.Label
            $groupBox.AutoSize = $true
            $groupBox.Padding = New-Object System.Windows.Forms.Padding(12, 6, 12, 12)
            $groupBox.Margin = New-Object System.Windows.Forms.Padding(0, 0, 0, 10)
            $groupBox.Anchor = ([System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right)

            # GroupBox.AutoSize 只看子控件的 PreferredSize;Dock=Fill 的子控件会被忽略而让分组塌成标题高。
            # 让 fieldGrid 自身 AutoSize + Dock=Top,分组才能撑到内容高度,同时宽度随分组拉伸。
            $fieldGrid = New-Object System.Windows.Forms.TableLayoutPanel
            $fieldGrid.AutoSize = $true
            $fieldGrid.AutoSizeMode = 'GrowAndShrink'
            $fieldGrid.Dock = 'Top'
            $fieldGrid.ColumnCount = 2
            [void]$fieldGrid.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
            [void]$fieldGrid.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100)))
            $groupBox.Controls.Add($fieldGrid)

            $fieldRow = 0
            foreach ($field in $group.Fields) {
                $fieldRow += New-GUIFieldRow -Field $field -Grid $fieldGrid -Row $fieldRow
            }

            [void]$pageGrid.RowStyles.Add((New-GUIRowStyle))
            $pageGrid.Controls.Add($groupBox, 0, $groupRow)
            $groupRow++
        }
    }

    $log = New-Object System.Windows.Forms.TextBox
    $log.Multiline = $true
    $log.ReadOnly = $true
    $log.ScrollBars = 'Vertical'
    $log.Dock = 'Fill'
    $log.Margin = New-Object System.Windows.Forms.Padding(0, 8, 0, 8)
    try { $log.Font = New-Object System.Drawing.Font('Consolas', 9) } catch { }
    $root.Controls.Add($log, 0, 1)
    $Script:refs.LogTextBox = $log

    $bottom = New-Object System.Windows.Forms.TableLayoutPanel
    $bottom.Dock = 'Fill'
    $bottom.ColumnCount = 2
    $bottom.RowCount = 1
    [void]$bottom.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100)))
    [void]$bottom.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::AutoSize)))
    [void]$bottom.RowStyles.Add((New-GUIRowStyle))

    $cfgFileLabel = New-Object System.Windows.Forms.Label
    $cfgFileLabel.Text = Get-GUIText 'Label.CfgFileHead'
    $cfgFileLabel.Dock = 'Fill'
    $cfgFileLabel.AutoSize = $false
    $cfgFileLabel.TextAlign = 'MiddleLeft'
    $bottom.Controls.Add($cfgFileLabel, 0, 0)
    $Script:refs.CfgFileLabel = $cfgFileLabel

    $rightFlow = New-Object System.Windows.Forms.FlowLayoutPanel
    $rightFlow.AutoSize = $true
    $rightFlow.AutoSizeMode = 'GrowAndShrink'
    $rightFlow.WrapContents = $false
    $rightFlow.FlowDirection = 'LeftToRight'
    $rightFlow.Anchor = [System.Windows.Forms.AnchorStyles]::Right

    $loadCfgButton = New-GUIButton (Get-GUIText 'Button.LoadCfg')
    $saveCfgButton = New-GUIButton (Get-GUIText 'Button.SaveCfg')
    $saveCfg2OtherFileButton = New-GUIButton (Get-GUIText 'Button.SaveAsCfg')
    $darkModeSetButton = New-GUIButton (Get-GUIText 'Button.DarkMode')
    $bgmSetButton = New-GUIButton (Get-GUIText 'Button.BGM')
    # 图标在 DarkMode.ps1 里按主题着色后挂到 Image 上,这里只负责排布。
    $darkModeSetButton.TextImageRelation = 'ImageBeforeText'
    $bgmSetButton.TextImageRelation = 'ImageBeforeText'
    $compileButton = New-GUIButton (Get-GUIText 'Button.Compile')
    $compileButton.Font = New-Object System.Drawing.Font($form.Font, [System.Drawing.FontStyle]::Bold)
    $compileButton.Margin = New-Object System.Windows.Forms.Padding(12, 0, 0, 0)
    $compileButton.MinimumSize = New-Object System.Drawing.Size(110, 30)

    $rightFlow.Controls.Add($loadCfgButton)
    $rightFlow.Controls.Add($saveCfgButton)
    $rightFlow.Controls.Add($saveCfg2OtherFileButton)
    $rightFlow.Controls.Add($darkModeSetButton)
    $rightFlow.Controls.Add($bgmSetButton)
    $rightFlow.Controls.Add($compileButton)
    $bottom.Controls.Add($rightFlow, 1, 0)
    $root.Controls.Add($bottom, 0, 2)

    $Script:refs.LoadCfgButton = $loadCfgButton
    $Script:refs.SaveCfgButton = $saveCfgButton
    $Script:refs.SaveCfg2OtherFileButton = $saveCfg2OtherFileButton
    $Script:refs.DarkModeSetButton = $darkModeSetButton
    $Script:refs.BGMSetButton = $bgmSetButton
    $Script:refs.CompileButton = $compileButton

    $cancelButton = New-Object System.Windows.Forms.Button
    $cancelButton.Visible = $false
    $form.Controls.Add($cancelButton)
    $form.CancelButton = $cancelButton
    $Script:refs.CancelButton = $cancelButton

    $Script:refs.CompileFileTextBox = $Script:FieldControls['inputFile']
    $Script:refs.OutputFileTextBox = $Script:FieldControls['outputFile']

    return $form
}

function New-GUIDialogs {
    # 仅在 GUI 文案提供了合法的过滤器语法("描述|模式")时才设置,避免缺翻译时崩。
    function Set-GUIDialogFilter($dialog, [string]$key) {
        $filter = Get-GUIText $key
        if ($filter -like '*|*') { $dialog.Filter = $filter }
    }

    $dialogs = @{}
    $dialogs.Compile = New-Object System.Windows.Forms.OpenFileDialog
    $dialogs.Compile.Title = Get-GUIText 'Dialog.Compile.Title'
    Set-GUIDialogFilter $dialogs.Compile 'Dialog.Compile.Filter'
    $dialogs.Compile.DefaultExt = 'ps1'

    $dialogs.Output = New-Object System.Windows.Forms.SaveFileDialog
    $dialogs.Output.Title = Get-GUIText 'Dialog.Output.Title'
    Set-GUIDialogFilter $dialogs.Output 'Dialog.Output.Filter'
    $dialogs.Output.DefaultExt = 'exe'

    $dialogs.Icon = New-Object System.Windows.Forms.OpenFileDialog
    $dialogs.Icon.Title = Get-GUIText 'Dialog.Icon.Title'
    Set-GUIDialogFilter $dialogs.Icon 'Dialog.Icon.Filter'
    $dialogs.Icon.DefaultExt = 'ico'

    $dialogs.Certificate = New-Object System.Windows.Forms.OpenFileDialog
    $dialogs.Certificate.Title = Get-GUIText 'Dialog.Certificate.Title'
    Set-GUIDialogFilter $dialogs.Certificate 'Dialog.Certificate.Filter'
    $dialogs.Certificate.DefaultExt = 'pfx'

    $dialogs.Folder = New-Object System.Windows.Forms.FolderBrowserDialog
    $dialogs.Folder.Description = Get-GUIText 'Dialog.Folder.Title'

    $dialogs.OpenCfg = New-Object System.Windows.Forms.OpenFileDialog
    $dialogs.OpenCfg.Title = Get-GUIText 'Dialog.OpenCfg.Title'
    Set-GUIDialogFilter $dialogs.OpenCfg 'Dialog.OpenCfg.Filter'

    $dialogs.SaveCfg = New-Object System.Windows.Forms.SaveFileDialog
    $dialogs.SaveCfg.Title = Get-GUIText 'Dialog.SaveCfg.Title'
    Set-GUIDialogFilter $dialogs.SaveCfg 'Dialog.SaveCfg.Filter'

    $Script:dialogs = $dialogs

    return $dialogs
}