src/GUI/Functions.ps1

# GUI 的 UIData ↔ 控件双向映射、点号路径工具、配置文件读写与 ps12exe 参数组装。

# 遍历 schema 的所有字段并缓存,避免每次调用都重新展开。
function Get-GUIAllFields {
    if (-not $Script:GUISchemaFieldList) {
        $list = New-Object System.Collections.ArrayList
        $map = @{}
        foreach ($page in (Get-GUISchema).Pages) {
            foreach ($group in $page.Groups) {
                foreach ($field in $group.Fields) {
                    [void]$list.Add($field)
                    $map[$field.Path] = $field
                }
            }
        }
        $Script:GUISchemaFieldList = $list
        $Script:GUISchemaFieldMap = $map
    }
    return $Script:GUISchemaFieldList
}

function Get-GUIField {
    param([string]$Path)
    [void](Get-GUIAllFields)
    return $Script:GUISchemaFieldMap[$Path]
}

# 点号路径读写嵌套哈希:中间层不存在则创建(只支持哈希表,不支持数组下标)。
function Get-NestedValue {
    param(
        [System.Collections.IDictionary]$UIData,
        [string]$Path
    )
    $current = $UIData
    foreach ($part in ($Path -split '\.')) {
        if ($current -isnot [System.Collections.IDictionary]) { return $null }
        if (-not $current.Contains($part)) { return $null }
        $current = $current[$part]
    }
    return $current
}

function Set-NestedValue {
    param(
        [System.Collections.IDictionary]$UIData,
        [string]$Path,
        $Value
    )
    $parts = $Path -split '\.'
    $current = $UIData
    for ($i = 0; $i -lt $parts.Count - 1; $i++) {
        $part = $parts[$i]
        if (-not $current.Contains($part) -or $current[$part] -isnot [System.Collections.IDictionary]) {
            $current[$part] = @{}
        }
        $current = $current[$part]
    }
    $current[$parts[-1]] = $Value
}

# 从以点号路径/键名为索引的哈希取引用,未就绪时安全返回 $null。
function Get-GUIRef {
    param($Table, [string]$Key)
    if ($Table -is [System.Collections.IDictionary] -and $Table.Contains($Key)) { return $Table[$Key] }
    return $null
}

function Get-FieldControl {
    param([string]$Path)
    return Get-GUIRef $Script:FieldControls $Path
}

function Get-FieldBrowseControl {
    param([string]$Path)
    return Get-GUIRef $Script:FieldBrowse $Path
}

# 递归枚举某个容器下的所有子控件(Flags 的选项 CheckBox 可能嵌在面板里)。
function Get-DescendantControls {
    param([System.Windows.Forms.Control]$Control)
    foreach ($child in $Control.Controls) {
        $child
        if ($child.HasChildren) { Get-DescendantControls $child }
    }
}

function Get-FlagValues {
    param($Container)
    @(Get-DescendantControls $Container | Where-Object { $_ -is [System.Windows.Forms.CheckBox] -and $_.Checked } | ForEach-Object { $_.Text })
}

function Get-DllExportCellText {
    param($Row, [string]$ColumnName)
    return [string]$Row.Cells[$Row.DataGridView.Columns[$ColumnName].Index].Value
}

# DataGridView → DllExports 数组。Params 为逗号分隔的 "type name",缺名字时自动 argN。
function Get-DllExportsValue {
    param($Grid)
    $result = @()
    foreach ($row in $Grid.Rows) {
        if ($row.IsNewRow) { continue }
        $funcName = Get-DllExportCellText $row 'FuncName'
        if (-not $funcName) { continue }
        $returnType = Get-DllExportCellText $row 'ReturnType'
        $paramsText = Get-DllExportCellText $row 'Params'
        $params = @()
        $index = 0
        foreach ($item in ($paramsText -split ',')) {
            $item = $item.Trim()
            if (-not $item) { continue }
            $index++
            $tokens = @($item -split '\s+' | Where-Object { $_ })
            if ($tokens.Count -ge 2) {
                $type = $tokens[0]
                $name = ($tokens[1..($tokens.Count - 1)] -join ' ')
            }
            else {
                $type = $tokens[0]
                $name = "arg$index"
            }
            $params += @{ type = $type; name = $name }
        }
        $result += @{ funcName = $funcName; returnType = $returnType; params = $params }
    }
    return $result
}

function Get-FieldValue {
    param($Field, $Control)
    if (-not $Control) { return $Field.Default }
    switch ($Field.Kind) {
        'Bool' { return [bool]$Control.Checked }
        'Choice' {
            if ($null -ne $Control.SelectedItem) { return [string]$Control.SelectedItem }
            return [string]$Control.Text
        }
        'Flags' { return @(Get-FlagValues $Control) }
        'DllExports' { return @(Get-DllExportsValue $Control) }
        default { return [string]$Control.Text }
    }
}

function Set-DllExportsValue {
    param($Grid, $Exports)
    $Grid.Rows.Clear()
    foreach ($export in $Exports) {
        $paramsText = (@($export.params) | ForEach-Object { "$($_.type) $($_.name)".Trim() }) -join ', '
        [void]$Grid.Rows.Add($export.funcName, $export.returnType, $paramsText)
    }
}

function Set-FieldValue {
    param($Field, $Value)
    $control = Get-FieldControl $Field.Path
    if (-not $control) { return }
    switch ($Field.Kind) {
        'Bool' { $control.Checked = [bool]$Value }
        'Choice' {
            $text = [string]$Value
            $items = @($control.Items | ForEach-Object { [string]$_ })
            if ($items -contains $text) { $control.SelectedItem = $text }
            else { $control.Text = $text }
        }
        'Flags' {
            $selected = @($Value | ForEach-Object { [string]$_ })
            foreach ($box in (Get-DescendantControls $control | Where-Object { $_ -is [System.Windows.Forms.CheckBox] })) {
                $box.Checked = $selected -contains $box.Text
            }
        }
        'DllExports' { Set-DllExportsValue $control @($Value) }
        default { $control.Text = [string]$Value }
    }
}

# 按 schema 默认值构造完整的 UIData(含被禁用字段)。
function Get-DefaultUIData {
    $data = @{ inputFile = ''; outputFile = ''; SchemaVersion = 2 }
    foreach ($field in Get-GUIAllFields) {
        Set-NestedValue $data $field.Path $field.Default
    }
    return $data
}

# 从控件读值,得到完整 UIData。
function Get-UIData {
    $data = @{ inputFile = ''; outputFile = ''; SchemaVersion = 2 }
    foreach ($field in Get-GUIAllFields) {
        $control = Get-FieldControl $field.Path
        Set-NestedValue $data $field.Path (Get-FieldValue $field $control)
    }
    return $data
}

# 把 UIData 写回控件(缺失键用 schema 默认值),随后刷新界面可用状态。
function Set-UIData {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary]$UIData
    )
    foreach ($field in Get-GUIAllFields) {
        $value = Get-NestedValue $UIData $field.Path
        if ($null -eq $value) { $value = $field.Default }
        Set-FieldValue $field $value
    }
    Update-UIState
}

function Update-ChoiceItems {
    param($Control, $Choices)
    $currentItems = @($Control.Items | ForEach-Object { [string]$_ })
    $same = $currentItems.Count -eq $Choices.Count
    if ($same) {
        for ($i = 0; $i -lt $Choices.Count; $i++) {
            if ($currentItems[$i] -ne $Choices[$i]) { $same = $false; break }
        }
    }
    if (-not $same) {
        $Control.BeginUpdate()
        try {
            $Control.Items.Clear()
            foreach ($choice in $Choices) { [void]$Control.Items.Add($choice) }
        }
        finally { $Control.EndUpdate() }
    }
    $current = if ($null -ne $Control.SelectedItem) { [string]$Control.SelectedItem } else { [string]$Control.Text }
    if ($Choices -contains $current) { $Control.SelectedItem = $current }
    elseif ($Control.Items.Count -gt 0) { $Control.SelectedIndex = 0 }
}

# 按 EnabledWhen 刷新每个字段的 Enabled;Choice 字段按 ChoicesWhen 重建选项。
function Update-UIState {
    if ($Script:UpdatingUI) { return }
    $Script:UpdatingUI = $true
    try {
        $data = Get-UIData
        foreach ($field in Get-GUIAllFields) {
            $control = Get-FieldControl $field.Path
            if (-not $control) { continue }
            $enabled = $true
            if ($field.EnabledWhen) { $enabled = [bool](& $field.EnabledWhen $data) }
            $control.Enabled = $enabled
            $browse = Get-FieldBrowseControl $field.Path
            if ($browse) { $browse.Enabled = $enabled }
            if ($field.Kind -eq 'Choice' -and $field.ChoicesWhen -and $enabled) {
                if (-not $control.Focused) {
                    $choices = @(& $field.ChoicesWhen $data | ForEach-Object { [string]$_ })
                    Update-ChoiceItems $control $choices
                }
            }
        }
    }
    finally {
        $Script:UpdatingUI = $false
    }
}

# URL 原样返回;',N' 资源索引只解析文件部分;相对路径以 BaseDir 为基准转绝对路径。
function Resolve-ProjectPath {
    param([string]$Path, [string]$BaseDir)
    if (-not $Path) { return '' }
    if ($Path -match '^[a-zA-Z][a-zA-Z0-9+.-]*://') { return $Path }
    if (-not $BaseDir) { return $Path }
    $suffix = ''
    if ($Path -match '^(.*?)(,\d+)$') {
        $Path = $Matches[1]
        $suffix = $Matches[2]
    }
    if ([System.IO.Path]::IsPathRooted($Path)) { return [System.IO.Path]::GetFullPath($Path) + $suffix }
    return [System.IO.Path]::GetFullPath((Join-Path $BaseDir $Path)) + $suffix
}

# 用 Uri.MakeRelativeUri 实现,兼容 Windows PowerShell 5.1。
function Get-RelativePath {
    param([string]$Path, [string]$BaseDir)
    if (-not $Path) { return '' }
    if (-not $BaseDir) { return $Path }
    $baseFull = [System.IO.Path]::GetFullPath($BaseDir).TrimEnd('\') + '\'
    $baseUri = [System.Uri]::new($baseFull)
    $fileUri = [System.Uri]::new([System.IO.Path]::GetFullPath($Path))
    $relative = [System.Uri]::UnescapeDataString($baseUri.MakeRelativeUri($fileUri).ToString())
    return ($relative -replace '/', '\')
}

function Test-ValueChanged {
    param($Value, $Default)
    if ($null -eq $Value) { return $false }
    if ($Value -is [array] -or $Default -is [array]) {
        return (ConvertTo-Json -InputObject @($Value) -Depth 16 -Compress) -ne (ConvertTo-Json -InputObject @($Default) -Depth 16 -Compress)
    }
    if ($Default -is [bool]) { return [bool]$Value -ne [bool]$Default }
    return [string]$Value -ne [string]$Default
}

# 只输出「字段可用且值与默认不同」的项,组装成 ps12exe 对象式 API。
function Get-ps12exeArgs {
    $data = Get-UIData
    $result = @{}
    $baseDir = ''
    if ($Script:ConfigFile) { $baseDir = Split-Path -Path $Script:ConfigFile -Parent }

    foreach ($field in Get-GUIAllFields) {
        $path = $field.Path
        # Signing 是合成组,单独处理。
        if ($path -like 'Signing.*') { continue }

        $enabled = $true
        if ($field.EnabledWhen) { $enabled = [bool](& $field.EnabledWhen $data) }
        if (-not $enabled) { continue }

        $value = Get-NestedValue $data $path
        if (-not (Test-ValueChanged $value $field.Default)) { continue }

        if ($path -eq 'Build.DllExports') {
            Set-NestedValue $result $path @($value)
            continue
        }
        if ($path -eq 'Build.Minify') {
            $value = [System.Management.Automation.Language.Parser]::ParseInput([string]$value, [ref]$null, [ref]$null).GetScriptBlock()
        }
        elseif ($path -in @('inputFile', 'outputFile', 'Build.TempDir', 'Resources.Icon')) {
            $value = Resolve-ProjectPath ([string]$value) $baseDir
        }
        Set-NestedValue $result $path $value
    }

    # Signing 合成组:Enabled 为假时整体不输出。
    if ([bool](Get-NestedValue $data 'Signing.Enabled')) {
        $signing = @{}
        foreach ($path in @('Signing.Certificate', 'Signing.Password', 'Signing.Thumbprint', 'Signing.Timestamp')) {
            $field = Get-GUIField $path
            if (-not $field) { continue }
            $enabled = $true
            if ($field.EnabledWhen) { $enabled = [bool](& $field.EnabledWhen $data) }
            if (-not $enabled) { continue }
            $value = Get-NestedValue $data $path
            if (-not (Test-ValueChanged $value $field.Default)) { continue }
            if ($path -eq 'Signing.Password') {
                $value = ConvertTo-SecureString ([string]$value) -AsPlainText -Force
            }
            elseif ($path -eq 'Signing.Certificate') {
                $value = Resolve-ProjectPath ([string]$value) $baseDir
            }
            Set-NestedValue $signing ($path -replace '^Signing\.', '') $value
        }
        $result.Signing = $signing
    }

    return $result
}

# 取 Layout 建好的文件/文件夹对话框(New-GUIDialogs 写入 $Script:dialogs)。
function Get-GUIDialog {
    param([string]$Key)
    return Get-GUIRef $Script:dialogs $Key
}

function SetCfgFile {
    param([string]$ConfigFile)
    $Script:ConfigFile = $ConfigFile
    if ($Script:refs -and $Script:refs.CfgFileLabel) {
        $Script:refs.CfgFileLabel.Text = (Get-GUIText 'Label.CfgFileHead') + $ConfigFile
    }
}

function LoadCfgFile {
    param([string]$ConfigFile)
    if (-not $ConfigFile) {
        $dialog = Get-GUIDialog 'OpenCfg'
        if (-not $dialog) { return }
        $previous = $dialog.FileName
        if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return }
        $ConfigFile = $dialog.FileName
        if (-not $ConfigFile -or $ConfigFile -eq $previous) { return }
    }
    try {
        $UIData = Import-Clixml -LiteralPath $ConfigFile
    }
    catch {
        [System.Windows.Forms.MessageBox]::Show((Get-GUIText 'Log.CfgLoadFailed') + $_.Exception.Message, (Get-GUIText 'Window.Title'), [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
        return
    }
    SetCfgFile $ConfigFile
    Set-UIData -UIData $UIData
    $Script:SavedSnapshot = Get-UIData
}

function SaveCfgFileAs {
    param([string]$ConfigFile)
    if (-not $ConfigFile) {
        $dialog = Get-GUIDialog 'SaveCfg'
        if (-not $dialog) { return }
        $previous = $dialog.FileName
        if ($dialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return }
        $ConfigFile = $dialog.FileName
        if (-not $ConfigFile -or $ConfigFile -eq $previous) { return }
    }
    Get-UIData | Export-Clixml -LiteralPath $ConfigFile
    SetCfgFile $ConfigFile
    $Script:SavedSnapshot = Get-UIData
}

function SaveCfgFile {
    param([string]$ConfigFile)
    if (-not $ConfigFile) { $ConfigFile = $Script:ConfigFile }
    if (-not $ConfigFile) { SaveCfgFileAs; return }
    SaveCfgFileAs $ConfigFile
}

function AskSaveCfg {
    $localize = $Script:LocalizeData
    return [System.Windows.Forms.MessageBox]::Show([string]$localize.AskSaveCfg, [string]$localize.AskSaveCfgTitle, [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question) -eq [System.Windows.Forms.DialogResult]::Yes
}

function Test-UIDirty {
    if ($null -eq $Script:SavedSnapshot) { return $false }
    $current = ConvertTo-Json -InputObject (Get-UIData) -Depth 16 -Compress
    $saved = ConvertTo-Json -InputObject $Script:SavedSnapshot -Depth 16 -Compress
    return $current -ne $saved
}

function Write-GUILog {
    param([string]$Text)
    $box = $Script:refs.LogTextBox
    if (-not $box) { return }
    $box.Text += "$Text`r`n"
    $box.SelectionStart = $box.Text.Length
    $box.ScrollToCaret()
}

function PauseMusic {
    [ps12exeGUI.Win32]::mciSendString("pause ps12exeGUIBGM", $null, 0, 0) | Out-Null
}
function ResumeMusic {
    [ps12exeGUI.Win32]::mciSendString("resume ps12exeGUIBGM", $null, 0, 0) | Out-Null
}