Private/PresentationWireParser.ps1

function Read-TerminalWireTaggedHashtable {
    param(
        [Parameter(Mandatory)][AllowNull()][object]$Node,
        [Parameter(Mandatory)][string]$Subject,
        [switch]$RequireCanonical
    )
    if ($Node -isnot [Collections.IDictionary]) { throw "$Subject must be a tagged object in the current wire format." }
    $value = ConvertFrom-TerminalTaggedValue -Node $Node -RequireCanonical:$RequireCanonical
    return ConvertTo-TerminalHashtableRoot -Value $value -Subject $Subject
}

function Read-TerminalCurrentScalarValue {
    param(
        [Parameter(Mandatory)][Collections.IDictionary]$Data,
        [switch]$RequireCanonical
    )
    Assert-TerminalWireObject $Data @('Name','Kind','Value') 'Current wire table cell'
    if ($Data['Name'] -isnot [string] -or [string]::IsNullOrWhiteSpace($Data['Name'])) {
        throw 'Table column names must be non-empty strings in the current wire format.'
    }
    $kind = Read-TerminalWireEnum $Data['Kind'] ([TerminalSlides.Schema.V1.ScalarKind]) 'Current wire table cell Kind'
    if ($kind -eq [TerminalSlides.Schema.V1.ScalarKind]::Null) {
        if ($null -ne $Data['Value']) { throw 'Current wire Null table cell Value must be null.' }
        $text = [Management.Automation.Language.NullString]::Value
    }
    else {
        Assert-TerminalWireString $Data['Value'] 'Current wire table cell Value'
        $text = ConvertFrom-TerminalPersistedScalarText -Kind $kind.ToString() -Value $Data['Value']
    }
    $scalar = [TerminalSlides.Schema.V1.ScalarValue]::new($kind, $text)
    try { $decoded = ConvertFrom-TerminalScalarValue $scalar }
    catch { throw "Current wire table cell Value is invalid for kind '$kind': $($_.Exception.Message)" }
    if ($RequireCanonical -and $kind -ne [TerminalSlides.Schema.V1.ScalarKind]::Null) {
        if ($kind -eq [TerminalSlides.Schema.V1.ScalarKind]::DateTime) {
            Assert-TerminalCanonicalTaggedScalarText -Type DateTime -Value $Data['Value']
        }
        else {
            $canonical = ConvertTo-TerminalScalarValue $decoded
            if ($canonical.Value -cne $Data['Value']) {
                throw "Current wire table cell Value for kind '$kind' is not canonical."
            }
        }
    }
    return [TerminalSlides.Schema.V1.DataCell]::new([string]$Data['Name'], $scalar)
}

function Read-TerminalCurrentPayload {
    param(
        [Parameter(Mandatory)][TerminalSlides.Schema.V1.ElementKind]$Kind,
        [AllowNull()][object]$Data,
        [switch]$RequireCanonical
    )
    $fields = switch ($Kind.ToString()) {
        { $_ -in 'Title','Subtitle','Text','Bullet','Box' } { @('Text') }
        'Code' { @('Code','Language') }
        'Image' { @('Path','AltText') }
        'Quote' { @('Text','Attribution') }
        'Chart' { @('ChartKind','Title','Points') }
        'Diagram' { @('Nodes','Edges') }
        'Table' { @('Rows') }
    }
    Assert-TerminalWireObject $Data $fields "Current wire $Kind payload"
    switch ($Kind.ToString()) {
        { $_ -in 'Title','Subtitle','Text','Bullet','Box' } {
            Assert-TerminalWireString $Data['Text'] "Current wire $Kind payload Text"
            return [TerminalSlides.Schema.V1.TextPayload]::new([string]$Data['Text'])
        }
        'Code' {
            Assert-TerminalWireString $Data['Code'] 'Current wire Code payload Code'
            Assert-TerminalWireString $Data['Language'] 'Current wire Code payload Language'
            if ([string]::IsNullOrWhiteSpace($Data['Language'])) { throw 'Current wire Code payload Language must be nonblank.' }
            return [TerminalSlides.Schema.V1.CodePayload]::new([string]$Data['Code'], [string]$Data['Language'])
        }
        'Image' {
            Assert-TerminalWireString $Data['Path'] 'Current wire Image payload Path'
            Assert-TerminalWireString $Data['AltText'] 'Current wire Image payload AltText' -AllowNull
            if ([string]::IsNullOrWhiteSpace($Data['Path'])) { throw 'Current wire Image payload Path must be nonblank.' }
            return [TerminalSlides.Schema.V1.ImagePayload]::new([string]$Data['Path'], [string]$Data['AltText'])
        }
        'Quote' {
            Assert-TerminalWireString $Data['Text'] 'Current wire Quote payload Text'
            Assert-TerminalWireString $Data['Attribution'] 'Current wire Quote payload Attribution' -AllowNull
            return [TerminalSlides.Schema.V1.QuotePayload]::new([string]$Data['Text'], [string]$Data['Attribution'])
        }
        'Chart' {
            $chartKind = Read-TerminalWireEnum $Data['ChartKind'] ([TerminalSlides.Schema.V1.ChartKind]) 'Current wire ChartKind'
            Assert-TerminalWireString $Data['Title'] 'Current wire Chart payload Title' -AllowNull
            Assert-TerminalWireArray $Data['Points'] 'Current wire Chart payload Points'
            $points = foreach ($point in $Data['Points']) {
                Assert-TerminalWireObject $point @('Label','Value') 'Current wire chart point'
                Assert-TerminalWireString $point['Label'] 'Current wire chart point Label'
                Assert-TerminalWireString $point['Value'] 'Current wire chart point Value'
                try { $number = [decimal]::Parse($point['Value'], [Globalization.CultureInfo]::InvariantCulture) }
                catch { throw "Current wire chart point Value '$($point['Value'])' is not a valid Decimal." }
                if ($RequireCanonical -and $number.ToString([Globalization.CultureInfo]::InvariantCulture) -cne $point['Value']) {
                    throw "Current wire chart point Value '$($point['Value'])' is not canonical."
                }
                [TerminalSlides.Schema.V1.ChartPoint]::new([string]$point['Label'], $number)
            }
            return [TerminalSlides.Schema.V1.ChartPayload]::new([TerminalSlides.Schema.V1.ChartPoint[]]@($points), $chartKind, [string]$Data['Title'])
        }
        'Diagram' {
            Assert-TerminalWireArray $Data['Nodes'] 'Current wire Diagram payload Nodes'
            Assert-TerminalWireArray $Data['Edges'] 'Current wire Diagram payload Edges'
            $nodes = foreach ($node in $Data['Nodes']) {
                Assert-TerminalWireObject $node @('Id','Label') 'Current wire diagram node'
                Assert-TerminalWireString $node['Id'] 'Current wire diagram node Id'
                Assert-TerminalWireString $node['Label'] 'Current wire diagram node Label'
                if ([string]::IsNullOrWhiteSpace($node['Id'])) { throw 'Current wire diagram node Id must be nonblank.' }
                [TerminalSlides.Schema.V1.DiagramNode]::new([string]$node['Id'], [string]$node['Label'])
            }
            $edges = foreach ($edge in $Data['Edges']) {
                Assert-TerminalWireObject $edge @('From','To','Label') 'Current wire diagram edge'
                Assert-TerminalWireString $edge['From'] 'Current wire diagram edge From'
                Assert-TerminalWireString $edge['To'] 'Current wire diagram edge To'
                Assert-TerminalWireString $edge['Label'] 'Current wire diagram edge Label' -AllowNull
                if ([string]::IsNullOrWhiteSpace($edge['From']) -or [string]::IsNullOrWhiteSpace($edge['To'])) {
                    throw 'Current wire diagram edge endpoints must be nonblank.'
                }
                [TerminalSlides.Schema.V1.DiagramEdge]::new([string]$edge['From'], [string]$edge['To'], [string]$edge['Label'])
            }
            return [TerminalSlides.Schema.V1.DiagramPayload]::new(
                [TerminalSlides.Schema.V1.DiagramNode[]]@($nodes), [TerminalSlides.Schema.V1.DiagramEdge[]]@($edges)
            )
        }
        'Table' {
            Assert-TerminalWireArray $Data['Rows'] 'Current wire table Rows'
            $rows = foreach ($row in $Data['Rows']) {
                Assert-TerminalWireObject $row @('Cells') 'Current wire table row'
                Assert-TerminalWireArray $row['Cells'] 'Current wire table row Cells'
                $cells = foreach ($cell in $row['Cells']) {
                    Read-TerminalCurrentScalarValue -Data $cell -RequireCanonical:$RequireCanonical
                }
                [TerminalSlides.Schema.V1.DataRow]::new([TerminalSlides.Schema.V1.DataCell[]]@($cells))
            }
            $typedRows = [TerminalSlides.Schema.V1.DataRow[]]@($rows)
            Assert-TerminalTableColumnIdentity -Rows $typedRows
            return [TerminalSlides.Schema.V1.TablePayload]::new($typedRows)
        }
    }
}

function Read-TerminalCurrentElement {
    param([AllowNull()][object]$Data, [switch]$RequireCanonical)
    $fields = @('Id','Kind','Payload','Region','X','Y','Width','Height','Alignment','VerticalAlignment','Padding',
        'ForegroundColor','BackgroundColor','Border','BorderStyle','RevealStep','OverflowBehavior')
    Assert-TerminalWireObject $Data $fields 'Current wire slide element'
    Assert-TerminalWireString $Data['Id'] 'Current wire element Id' -AllowNull
    $kind = Read-TerminalWireEnum $Data['Kind'] ([TerminalSlides.Schema.V1.ElementKind]) 'Current wire element Kind'
    $payload = Read-TerminalCurrentPayload -Kind $kind -Data $Data['Payload'] -RequireCanonical:$RequireCanonical
    $element = [TerminalSlides.Schema.V1.SlideElement]::new($kind, $payload)
    $element.Id = [string]$Data['Id']
    $element.Region = Read-TerminalWireDomainName $Data['Region'] $script:TerminalElementRegionOrder 'Current wire element Region'
    $element.Alignment = Read-TerminalWireDomainName $Data['Alignment'] @('Left','Center','Right') 'Current wire element Alignment'
    $element.VerticalAlignment = Read-TerminalWireDomainName $Data['VerticalAlignment'] @('Top','Middle','Bottom') 'Current wire element VerticalAlignment'
    $element.ForegroundColor = Read-TerminalWireHexColor $Data['ForegroundColor'] 'Current wire element ForegroundColor' -AllowDefault
    $element.BackgroundColor = Read-TerminalWireHexColor $Data['BackgroundColor'] 'Current wire element BackgroundColor' -AllowDefault
    $element.BorderStyle = Read-TerminalWireDomainName $Data['BorderStyle'] @('unicode','ascii','double','rounded','single') 'Current wire element BorderStyle'
    $element.OverflowBehavior = Read-TerminalWireDomainName $Data['OverflowBehavior'] @('Wrap','Truncate','Scroll') 'Current wire element OverflowBehavior'
    foreach ($field in 'X','Y','Width','Height','Padding') { $element.$field = Read-TerminalWireInt32 $Data[$field] "Current wire element $field" }
    $element.RevealStep = Read-TerminalWireInt32 $Data['RevealStep'] 'Current wire element RevealStep'
    Assert-TerminalWireBoolean $Data['Border'] 'Current wire element Border'
    $element.Border = [bool]$Data['Border']
    return $element
}

function Read-TerminalCurrentSlide {
    param([AllowNull()][object]$Data, [switch]$RequireCanonical)
    $fields = @('Id','Index','Title','Layout','Notes','Background','Transition','Hidden','MaxRevealStep','Metadata','Elements')
    Assert-TerminalWireObject $Data $fields 'Current wire slide'
    $slide = [TerminalSlides.Schema.V1.Slide]::new()
    foreach ($field in 'Id','Title','Notes','Transition') { Assert-TerminalWireString $Data[$field] "Current wire slide $field" -AllowNull; $slide.$field = $Data[$field] }
    $slide.Index = Read-TerminalWireInt32 $Data['Index'] 'Current wire slide Index' 0
    $slide.Layout = Read-TerminalWireDomainName $Data['Layout'] $script:TerminalSlideLayouts 'Current wire slide Layout'
    $slide.Background = Read-TerminalWireHexColor $Data['Background'] 'Current wire slide Background' -AllowDefault
    Assert-TerminalWireBoolean $Data['Hidden'] 'Current wire slide Hidden'; $slide.Hidden = [bool]$Data['Hidden']
    [void](Read-TerminalWireInt32 $Data['MaxRevealStep'] 'Current wire slide MaxRevealStep')
    Assert-TerminalWireObject $Data['Metadata'] @('Author','Custom') 'Current wire slide Metadata'
    Assert-TerminalWireString $Data.Metadata['Author'] 'Current wire slide metadata Author' -AllowNull
    $slide.Metadata.Author = $Data.Metadata['Author']
    $slide.Metadata.Custom = Read-TerminalWireTaggedHashtable $Data.Metadata['Custom'] 'Current wire slide Metadata Custom' -RequireCanonical:$RequireCanonical
    Assert-TerminalWireArray $Data['Elements'] 'Current wire slide Elements'
    foreach ($elementData in $Data['Elements']) { $slide.Elements.Add((Read-TerminalCurrentElement $elementData -RequireCanonical:$RequireCanonical)) }
    $slide.MaxRevealStep = Get-TerminalSlideMaximumRevealStep -Slide $slide
    return $slide
}

function Read-TerminalCurrentThemeDefinition {
    param([AllowNull()][object]$Data, [switch]$RequireCanonical)
    $fields = @('Name','Background','Foreground','Primary','Accent','Muted','Heading','Border','CodeTheme','CodeBackground',
        'CodeForeground','BulletSymbol','BoxDrawingStyle','HeadingStyle','ChartPalette','ErrorColor','WarningColor','SuccessColor','Metadata')
    Assert-TerminalWireObject $Data $fields 'Current wire ThemeDefinition'
    $theme = [TerminalSlides.Schema.V1.ThemeDefinition]::new()
    foreach ($field in 'Name','CodeTheme','BulletSymbol') {
        Assert-TerminalWireString $Data[$field] "Current wire ThemeDefinition $field"
        if ([string]::IsNullOrWhiteSpace($Data[$field])) { throw "Current wire ThemeDefinition $field must be nonblank." }
        $theme.$field = $Data[$field]
    }
    foreach ($field in 'Background','Foreground','Primary','Accent','Muted','Heading','Border','ErrorColor','WarningColor','SuccessColor') {
        $theme.$field = Read-TerminalWireHexColor $Data[$field] "Current wire ThemeDefinition $field"
    }
    foreach ($field in 'CodeBackground','CodeForeground') { $theme.$field = Read-TerminalWireHexColor $Data[$field] "Current wire ThemeDefinition $field" -AllowDefault }
    $theme.BoxDrawingStyle = Read-TerminalWireDomainName $Data['BoxDrawingStyle'] @('unicode','ascii','double','rounded','single') 'Current wire ThemeDefinition BoxDrawingStyle'
    $theme.HeadingStyle = Read-TerminalWireDomainName $Data['HeadingStyle'] @('plain','bold','banner') 'Current wire ThemeDefinition HeadingStyle'
    Assert-TerminalWireArray $Data['ChartPalette'] 'Current wire ThemeDefinition ChartPalette'
    if ($Data['ChartPalette'].Count -eq 0) { throw 'Current wire ThemeDefinition ChartPalette must not be empty.' }
    $theme.ChartPalette = [string[]]@($Data['ChartPalette'] | ForEach-Object { Read-TerminalWireHexColor $_ 'Current wire ThemeDefinition ChartPalette' })
    $theme.Metadata = Read-TerminalWireTaggedHashtable $Data['Metadata'] 'Current wire ThemeDefinition Metadata' -RequireCanonical:$RequireCanonical
    return $theme
}

function Read-TerminalCurrentPresentation {
    param([AllowNull()][object]$Data, [switch]$RequireThemeDefinition)
    $fields = @('Title','Subtitle','Author','Description','Theme','Width','Height','DefaultTransition','DefaultLayout',
        'CreatedDate','ModifiedDate','Metadata','Configuration','Slides')
    if ($RequireThemeDefinition) { $fields = @($fields[0..4] + 'ThemeDefinition' + $fields[5..($fields.Count - 1)]) }
    Assert-TerminalWireObject $Data $fields 'Current wire Presentation'
    $presentation = [TerminalSlides.Schema.V1.TerminalPresentation]::new()
    foreach ($field in 'Title','Subtitle','Author','Description','DefaultTransition') {
        Assert-TerminalWireString $Data[$field] "Current wire Presentation $field" -AllowNull
        $presentation.$field = $Data[$field]
    }
    if ($RequireThemeDefinition) {
        Assert-TerminalWireString $Data['Theme'] 'Current wire Presentation Theme'
        $theme = Read-TerminalCurrentThemeDefinition $Data['ThemeDefinition'] -RequireCanonical
        if ($Data['Theme'] -cne $theme.Name) { throw 'Current wire Presentation Theme must exactly match ThemeDefinition Name.' }
        $presentation.Theme = $theme.Name
        $presentation.EmbeddedTheme = $theme
    }
    else {
        Assert-TerminalWireString $Data['Theme'] 'Current wire Presentation Theme'
        try {
            $resolvedTheme = Get-ResolvedTheme -Name $Data['Theme']
            $presentation.Theme = $resolvedTheme.Name
            $presentation.EmbeddedTheme = Copy-TerminalThemeDefinition $resolvedTheme
        }
        catch { throw "Current wire Presentation Theme has unsupported value '$($Data['Theme'])'." }
    }
    $presentation.Width = Read-TerminalWireInt32 $Data['Width'] 'Current wire Presentation Width'
    $presentation.Height = Read-TerminalWireInt32 $Data['Height'] 'Current wire Presentation Height'
    $presentation.DefaultLayout = Read-TerminalWireDomainName $Data['DefaultLayout'] $script:TerminalSlideLayouts 'Current wire Presentation DefaultLayout'
    foreach ($field in 'CreatedDate','ModifiedDate') {
        Assert-TerminalWireString $Data[$field] "Current wire Presentation $field"
        if ($RequireThemeDefinition) {
            try { Assert-TerminalCanonicalTaggedScalarText -Type DateTime -Value $Data[$field] }
            catch { throw "Current wire Presentation $field is invalid: $($_.Exception.Message)" }
        }
    }
    try { $presentation.CreatedDate = ConvertFrom-TerminalDateTimeText $Data['CreatedDate'] }
    catch { throw "Current wire Presentation CreatedDate is invalid: $($_.Exception.Message)" }
    try { $modified = ConvertFrom-TerminalDateTimeText $Data['ModifiedDate'] }
    catch { throw "Current wire Presentation ModifiedDate is invalid: $($_.Exception.Message)" }
    Assert-TerminalWireObject $Data['Metadata'] @('Title','Subtitle','Author','Description','Version','Custom') 'Current wire Presentation Metadata'
    foreach ($field in 'Title','Subtitle','Author','Description','Version') {
        Assert-TerminalWireString $Data.Metadata[$field] "Current wire presentation metadata $field" -AllowNull
        $presentation.Metadata.$field = $Data.Metadata[$field]
    }
    $presentation.Metadata.Custom = Read-TerminalWireTaggedHashtable $Data.Metadata['Custom'] 'Current wire Presentation Metadata Custom' -RequireCanonical:$RequireThemeDefinition
    $presentation.Configuration = Read-TerminalWireTaggedHashtable $Data['Configuration'] 'Current wire Presentation Configuration' -RequireCanonical:$RequireThemeDefinition
    Assert-TerminalWireArray $Data['Slides'] 'Current wire Presentation Slides'
    foreach ($slideData in $Data['Slides']) { $presentation.Slides.Add((Read-TerminalCurrentSlide $slideData -RequireCanonical:$RequireThemeDefinition)) }
    Update-SlideIndices $presentation
    $presentation.ModifiedDate = $modified
    try { Assert-TerminalWireReadyPresentationModel -Presentation $presentation }
    catch { throw "Current wire Presentation is semantically invalid: $($_.Exception.Message)" }
    return $presentation
}

function ConvertFrom-TerminalCurrentData {
    param([Parameter(Mandatory)][Collections.IDictionary]$Envelope)
    Assert-TerminalExactObjectFields $Envelope @('$schema','SchemaVersion','Presentation') 'Current wire envelope'
    Assert-TerminalWireString $Envelope['$schema'] 'Current wire envelope $schema'
    $version = Read-TerminalWireInt32 $Envelope['SchemaVersion'] 'Current wire envelope SchemaVersion' 1
    $isCurrent = $Envelope['$schema'] -ceq $script:TerminalSlidesWireSchema -and $version -eq $script:TerminalSlidesWireVersion
    $isLegacy = $Envelope['$schema'] -ceq $script:TerminalSlidesLegacyWireSchema -and $version -eq $script:TerminalSlidesLegacyWireVersion
    if (-not $isCurrent -and -not $isLegacy) { throw "Unsupported TerminalSlides schema '$($Envelope['$schema'])' version '$version'." }
    return Read-TerminalCurrentPresentation $Envelope['Presentation'] -RequireThemeDefinition:$isCurrent
}