Private/Console/Tables.psm1

using namespace System
using namespace System.Collections.Generic
using namespace System.Linq

using module ..\Enums.psm1
using module ..\Abstracts.psm1
using module .\Rendering.psm1
using module .\Widgets.psm1
using module .\Boxes.psm1

class TableCell : IRenderable {
    [IRenderable]$Content
    [int]$ColumnSpan

    TableCell([IRenderable]$content) {
        $this.Content = $content
        $this.ColumnSpan = 1
    }

    [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) {
        return $this.Content.Measure($options, $maxWidth)
    }

    [object[]] Render([RenderOptions]$options, [int]$maxWidth) {
        return $this.Content.Render($options, $maxWidth)
    }
}

class TableColumn {
    [IRenderable]$Header
    [IRenderable]$Footer
    [Nullable[int]]$Width
    [Padding]$Padding
    [bool]$NoWrap
    [Nullable[Justify]]$Alignment

    TableColumn([string]$header) {
        $this.Header = [Markup]::new($header)
        $this.Padding = [Padding]::new(1, 1, 0, 0)
        $this.NoWrap = $false
    }

    TableColumn([IRenderable]$header) {
        $this.Header = $header
        $this.Padding = [Padding]::new(1, 1, 0, 0)
        $this.NoWrap = $false
    }
}

class TableRow {
    [List[IRenderable]]$Items
    [bool]$IsHeader
    [bool]$IsFooter

    TableRow([IEnumerable[IRenderable]]$items) {
        $this.Items = [List[IRenderable]]::new($items)
        $this.IsHeader = $false
        $this.IsFooter = $false
    }

    static [TableRow] Header([IEnumerable[IRenderable]]$items) {
        $row = [TableRow]::new($items)
        $row.IsHeader = $true
        return $row
    }

    static [TableRow] Footer([IEnumerable[IRenderable]]$items) {
        $row = [TableRow]::new($items)
        $row.IsFooter = $true
        return $row
    }
}

class TableRowCollection : System.Collections.IEnumerable {
    hidden [List[TableRow]]$_list
    hidden [object]$_table

    TableRowCollection([object]$table) {
        $this._list = [List[TableRow]]::new()
        $this._table = $table
    }

    [int] get_Count() { return $this._list.Count }

    [void] Add([TableRow]$row) {
        $this._list.Add($row)
    }

    [void] Insert([int]$index, [TableRow]$row) {
        $this._list.Insert($index, $row)
    }

    [void] RemoveAt([int]$index) {
        $this._list.RemoveAt($index)
    }

    [System.Collections.IEnumerator] GetEnumerator() {
        return $this._list.GetEnumerator()
    }
}

class TableTitle {
    [string]$Text
    [Style]$Style

    TableTitle([string]$text) {
        $this.Text = $text
        $this.Style = [Style]::Plain
    }
    TableTitle([string]$text, [Style]$style) {
        $this.Text = $text
        $this.Style = if ($null -ne $style) { $style } else { [Style]::Plain }
    }
}

class TableBorder {
    [bool] get_Visible() { return $true }
    [bool] get_UsePadding() { return $true }
    [bool] get_SupportsRowSeparator() { return $false }

    static [TableBorder] get_None() { return [NoTableBorder]::new() }
    static [TableBorder] get_Ascii() { return [AsciiTableBorder]::new() }
    static [TableBorder] get_Square() { return [SquareTableBorder]::new() }
    static [TableBorder] get_Rounded() { return [RoundedTableBorder]::new() }
    static [TableBorder] get_Heavy() { return [HeavyTableBorder]::new() }
    static [TableBorder] get_Double() { return [DoubleTableBorder]::new() }

    [string] GetPart([TableBorderPart]$part) { return "" }

    [string] GetColumnRow([TablePart]$part, [List[int]]$widths, [List[TableColumn]]$columns) {
        if (!$this.Visible) { return "" }
        $parts = $this.GetTableParts($part)
        $left = $parts[0]; $center = $parts[1]; $separator = $parts[2]; $right = $parts[3]

        $sb = [System.Text.StringBuilder]::new()
        [void]$sb.Append($left)
        for ($i=0; $i -lt $widths.Count; $i++) {
            $w = $widths[$i]
            $pad = if ($this.UsePadding) { $columns[$i].Padding.GetLeftSafe() + $columns[$i].Padding.GetRightSafe() } else { 0 }
            $w += $pad
            for ($j=0; $j -lt $w; $j++) { [void]$sb.Append($center) }
            if ($i -lt ($widths.Count - 1)) { [void]$sb.Append($separator) }
        }
        [void]$sb.Append($right)
        return $sb.ToString()
    }

    [string[]] GetTableParts([TablePart]$part) {
        switch ($part) {
            ([TablePart]::Top) { return @($this.GetPart([TableBorderPart]::HeaderTopLeft), $this.GetPart([TableBorderPart]::HeaderTop), $this.GetPart([TableBorderPart]::HeaderTopSeparator), $this.GetPart([TableBorderPart]::HeaderTopRight)) }
            ([TablePart]::HeaderSeparator) { return @($this.GetPart([TableBorderPart]::HeaderBottomLeft), $this.GetPart([TableBorderPart]::HeaderBottom), $this.GetPart([TableBorderPart]::HeaderBottomSeparator), $this.GetPart([TableBorderPart]::HeaderBottomRight)) }
            ([TablePart]::RowSeparator) { return @($this.GetPart([TableBorderPart]::RowLeft), $this.GetPart([TableBorderPart]::RowCenter), $this.GetPart([TableBorderPart]::RowSeparator), $this.GetPart([TableBorderPart]::RowRight)) }
            ([TablePart]::FooterSeparator) { return @($this.GetPart([TableBorderPart]::FooterTopLeft), $this.GetPart([TableBorderPart]::FooterTop), $this.GetPart([TableBorderPart]::FooterTopSeparator), $this.GetPart([TableBorderPart]::FooterTopRight)) }
            ([TablePart]::Bottom) { return @($this.GetPart([TableBorderPart]::FooterBottomLeft), $this.GetPart([TableBorderPart]::FooterBottom), $this.GetPart([TableBorderPart]::FooterBottomSeparator), $this.GetPart([TableBorderPart]::FooterBottomRight)) }
        }
        return @("", "", "", "")
    }

    [TableBorder] GetSafeBorder([bool]$safe) {
        if ($safe) { return [AsciiTableBorder]::new() }
        return $this
    }

    static [TableBorder] GetSafeBorder([RenderOptions]$options, [object]$borderable) {
        if ($null -eq $borderable -or -not ('Border' -in $borderable.PSObject.Properties.Name)) { return [NoTableBorder]::new() }
        $border = $borderable.Border
        if ($null -eq $border) { return [NoTableBorder]::new() }
        if ($borderable.PSObject.Properties.Name -contains 'UseSafeBorder' -and !$borderable.UseSafeBorder) { return $border }
        if (!$options.Unicode) { return $border.GetSafeBorder($true) }
        return $border
    }
}

class NoTableBorder : TableBorder {
    [bool] get_Visible() { return $false }
    [bool] get_UsePadding() { return $true }
    [string] GetPart([TableBorderPart]$part) { return "" }
}

class AsciiTableBorder : TableBorder {
    [bool] get_SupportsRowSeparator() { return $true }
    [string] GetPart([TableBorderPart]$part) {
        switch ($part) {
            ([TableBorderPart]::HeaderTopLeft) { return "+" }
            ([TableBorderPart]::HeaderTop) { return "-" }
            ([TableBorderPart]::HeaderTopSeparator) { return "+" }
            ([TableBorderPart]::HeaderTopRight) { return "+" }
            ([TableBorderPart]::HeaderLeft) { return "|" }
            ([TableBorderPart]::HeaderSeparator) { return "|" }
            ([TableBorderPart]::HeaderRight) { return "|" }
            ([TableBorderPart]::HeaderBottomLeft) { return "+" }
            ([TableBorderPart]::HeaderBottom) { return "-" }
            ([TableBorderPart]::HeaderBottomSeparator) { return "+" }
            ([TableBorderPart]::HeaderBottomRight) { return "+" }
            ([TableBorderPart]::CellLeft) { return "|" }
            ([TableBorderPart]::CellSeparator) { return "|" }
            ([TableBorderPart]::CellRight) { return "|" }
            ([TableBorderPart]::FooterTopLeft) { return "+" }
            ([TableBorderPart]::FooterTop) { return "-" }
            ([TableBorderPart]::FooterTopSeparator) { return "+" }
            ([TableBorderPart]::FooterTopRight) { return "+" }
            ([TableBorderPart]::FooterBottomLeft) { return "+" }
            ([TableBorderPart]::FooterBottom) { return "-" }
            ([TableBorderPart]::FooterBottomSeparator) { return "+" }
            ([TableBorderPart]::FooterBottomRight) { return "+" }
            ([TableBorderPart]::RowLeft) { return "+" }
            ([TableBorderPart]::RowCenter) { return "-" }
            ([TableBorderPart]::RowSeparator) { return "+" }
            ([TableBorderPart]::RowRight) { return "+" }
        }
        return ""
    }
}

class SquareTableBorder : TableBorder {
    [bool] get_SupportsRowSeparator() { return $true }
    [string] GetPart([TableBorderPart]$part) {
        switch ($part) {
            ([TableBorderPart]::HeaderTopLeft) { return "┌" }
            ([TableBorderPart]::HeaderTop) { return "─" }
            ([TableBorderPart]::HeaderTopSeparator) { return "┬" }
            ([TableBorderPart]::HeaderTopRight) { return "┐" }
            ([TableBorderPart]::HeaderLeft) { return "│" }
            ([TableBorderPart]::HeaderSeparator) { return "│" }
            ([TableBorderPart]::HeaderRight) { return "│" }
            ([TableBorderPart]::HeaderBottomLeft) { return "├" }
            ([TableBorderPart]::HeaderBottom) { return "─" }
            ([TableBorderPart]::HeaderBottomSeparator) { return "┼" }
            ([TableBorderPart]::HeaderBottomRight) { return "┤" }
            ([TableBorderPart]::CellLeft) { return "│" }
            ([TableBorderPart]::CellSeparator) { return "│" }
            ([TableBorderPart]::CellRight) { return "│" }
            ([TableBorderPart]::FooterTopLeft) { return "├" }
            ([TableBorderPart]::FooterTop) { return "─" }
            ([TableBorderPart]::FooterTopSeparator) { return "┼" }
            ([TableBorderPart]::FooterTopRight) { return "┤" }
            ([TableBorderPart]::FooterBottomLeft) { return "└" }
            ([TableBorderPart]::FooterBottom) { return "─" }
            ([TableBorderPart]::FooterBottomSeparator) { return "┴" }
            ([TableBorderPart]::FooterBottomRight) { return "┘" }
            ([TableBorderPart]::RowLeft) { return "├" }
            ([TableBorderPart]::RowCenter) { return "─" }
            ([TableBorderPart]::RowSeparator) { return "┼" }
            ([TableBorderPart]::RowRight) { return "┤" }
        }
        return ""
    }
}

class RoundedTableBorder : TableBorder {
    [bool] get_SupportsRowSeparator() { return $true }
    [string] GetPart([TableBorderPart]$part) {
        switch ($part) {
            ([TableBorderPart]::HeaderTopLeft) { return "╭" }
            ([TableBorderPart]::HeaderTop) { return "─" }
            ([TableBorderPart]::HeaderTopSeparator) { return "┬" }
            ([TableBorderPart]::HeaderTopRight) { return "╮" }
            ([TableBorderPart]::HeaderLeft) { return "│" }
            ([TableBorderPart]::HeaderSeparator) { return "│" }
            ([TableBorderPart]::HeaderRight) { return "│" }
            ([TableBorderPart]::HeaderBottomLeft) { return "├" }
            ([TableBorderPart]::HeaderBottom) { return "─" }
            ([TableBorderPart]::HeaderBottomSeparator) { return "┼" }
            ([TableBorderPart]::HeaderBottomRight) { return "┤" }
            ([TableBorderPart]::CellLeft) { return "│" }
            ([TableBorderPart]::CellSeparator) { return "│" }
            ([TableBorderPart]::CellRight) { return "│" }
            ([TableBorderPart]::FooterTopLeft) { return "├" }
            ([TableBorderPart]::FooterTop) { return "─" }
            ([TableBorderPart]::FooterTopSeparator) { return "┼" }
            ([TableBorderPart]::FooterTopRight) { return "┤" }
            ([TableBorderPart]::FooterBottomLeft) { return "╰" }
            ([TableBorderPart]::FooterBottom) { return "─" }
            ([TableBorderPart]::FooterBottomSeparator) { return "┴" }
            ([TableBorderPart]::FooterBottomRight) { return "╯" }
            ([TableBorderPart]::RowLeft) { return "├" }
            ([TableBorderPart]::RowCenter) { return "─" }
            ([TableBorderPart]::RowSeparator) { return "┼" }
            ([TableBorderPart]::RowRight) { return "┤" }
        }
        return ""
    }
}

class HeavyTableBorder : TableBorder {
    [bool] get_SupportsRowSeparator() { return $true }
    [string] GetPart([TableBorderPart]$part) {
        switch ($part) {
            ([TableBorderPart]::HeaderTopLeft) { return "┏" }
            ([TableBorderPart]::HeaderTop) { return "━" }
            ([TableBorderPart]::HeaderTopSeparator) { return "┳" }
            ([TableBorderPart]::HeaderTopRight) { return "┓" }
            ([TableBorderPart]::HeaderLeft) { return "┃" }
            ([TableBorderPart]::HeaderSeparator) { return "┃" }
            ([TableBorderPart]::HeaderRight) { return "┃" }
            ([TableBorderPart]::HeaderBottomLeft) { return "┣" }
            ([TableBorderPart]::HeaderBottom) { return "━" }
            ([TableBorderPart]::HeaderBottomSeparator) { return "╋" }
            ([TableBorderPart]::HeaderBottomRight) { return "┫" }
            ([TableBorderPart]::CellLeft) { return "┃" }
            ([TableBorderPart]::CellSeparator) { return "┃" }
            ([TableBorderPart]::CellRight) { return "┃" }
            ([TableBorderPart]::FooterTopLeft) { return "┣" }
            ([TableBorderPart]::FooterTop) { return "━" }
            ([TableBorderPart]::FooterTopSeparator) { return "╋" }
            ([TableBorderPart]::FooterTopRight) { return "┫" }
            ([TableBorderPart]::FooterBottomLeft) { return "┗" }
            ([TableBorderPart]::FooterBottom) { return "━" }
            ([TableBorderPart]::FooterBottomSeparator) { return "┻" }
            ([TableBorderPart]::FooterBottomRight) { return "┛" }
            ([TableBorderPart]::RowLeft) { return "┣" }
            ([TableBorderPart]::RowCenter) { return "━" }
            ([TableBorderPart]::RowSeparator) { return "╋" }
            ([TableBorderPart]::RowRight) { return "┫" }
        }
        return ""
    }
}

class DoubleTableBorder : TableBorder {
    [bool] get_SupportsRowSeparator() { return $true }
    [string] GetPart([TableBorderPart]$part) {
        switch ($part) {
            ([TableBorderPart]::HeaderTopLeft) { return "╔" }
            ([TableBorderPart]::HeaderTop) { return "═" }
            ([TableBorderPart]::HeaderTopSeparator) { return "╦" }
            ([TableBorderPart]::HeaderTopRight) { return "╗" }
            ([TableBorderPart]::HeaderLeft) { return "║" }
            ([TableBorderPart]::HeaderSeparator) { return "║" }
            ([TableBorderPart]::HeaderRight) { return "║" }
            ([TableBorderPart]::HeaderBottomLeft) { return "╠" }
            ([TableBorderPart]::HeaderBottom) { return "═" }
            ([TableBorderPart]::HeaderBottomSeparator) { return "╬" }
            ([TableBorderPart]::HeaderBottomRight) { return "╣" }
            ([TableBorderPart]::CellLeft) { return "║" }
            ([TableBorderPart]::CellSeparator) { return "║" }
            ([TableBorderPart]::CellRight) { return "║" }
            ([TableBorderPart]::FooterTopLeft) { return "╠" }
            ([TableBorderPart]::FooterTop) { return "═" }
            ([TableBorderPart]::FooterTopSeparator) { return "╬" }
            ([TableBorderPart]::FooterTopRight) { return "╣" }
            ([TableBorderPart]::FooterBottomLeft) { return "╚" }
            ([TableBorderPart]::FooterBottom) { return "═" }
            ([TableBorderPart]::FooterBottomSeparator) { return "╩" }
            ([TableBorderPart]::FooterBottomRight) { return "╝" }
            ([TableBorderPart]::RowLeft) { return "╠" }
            ([TableBorderPart]::RowCenter) { return "═" }
            ([TableBorderPart]::RowSeparator) { return "╬" }
            ([TableBorderPart]::RowRight) { return "╣" }
        }
        return ""
    }
}

class MarkdownTableBorder : TableBorder {
    [bool] get_SupportsRowSeparator() { return $false }
    [string] GetPart([TableBorderPart]$part) {
        switch ($part) {
            ([TableBorderPart]::HeaderTopLeft) { return " " }
            ([TableBorderPart]::HeaderTop) { return " " }
            ([TableBorderPart]::HeaderTopSeparator) { return " " }
            ([TableBorderPart]::HeaderTopRight) { return " " }
            ([TableBorderPart]::HeaderLeft) { return "|" }
            ([TableBorderPart]::HeaderSeparator) { return "|" }
            ([TableBorderPart]::HeaderRight) { return "|" }
            ([TableBorderPart]::HeaderBottomLeft) { return "|" }
            ([TableBorderPart]::HeaderBottom) { return "-" }
            ([TableBorderPart]::HeaderBottomSeparator) { return "|" }
            ([TableBorderPart]::HeaderBottomRight) { return "|" }
            ([TableBorderPart]::CellLeft) { return "|" }
            ([TableBorderPart]::CellSeparator) { return "|" }
            ([TableBorderPart]::CellRight) { return "|" }
            ([TableBorderPart]::FooterTopLeft) { return " " }
            ([TableBorderPart]::FooterTop) { return " " }
            ([TableBorderPart]::FooterTopSeparator) { return " " }
            ([TableBorderPart]::FooterTopRight) { return " " }
            ([TableBorderPart]::FooterBottomLeft) { return " " }
            ([TableBorderPart]::FooterBottom) { return " " }
            ([TableBorderPart]::FooterBottomSeparator) { return " " }
            ([TableBorderPart]::FooterBottomRight) { return " " }
            ([TableBorderPart]::RowLeft) { return " " }
            ([TableBorderPart]::RowCenter) { return " " }
            ([TableBorderPart]::RowSeparator) { return " " }
            ([TableBorderPart]::RowRight) { return " " }
        }
        return ""
    }
}
class TableMeasurer {
    hidden [object]$_table
    hidden [RenderOptions]$_options
    hidden [int]$_explicitWidth
    hidden [TableBorder]$_border
    hidden [bool]$_padRightCell

    TableMeasurer([object]$table, [RenderOptions]$options) {
        $this._table = $table
        $this._options = $options
        $this._explicitWidth = if ($null -ne $table.Width) { $table.Width } else { -1 }
        $this._border = $table.Border
        $this._padRightCell = $table.PadRightCell
    }

    [int] CalculateTotalCellWidth([int]$maxWidth) {
        $totalCellWidth = $maxWidth
        if ($this._explicitWidth -ne -1) {
            $totalCellWidth = [Math]::Min($this._explicitWidth, $maxWidth)
        }
        return $totalCellWidth - $this.GetNonColumnWidth()
    }

    [int] GetNonColumnWidth() {
        $hideBorder = !$this._border.get_Visible()
        $usePadding = $this._border.get_UsePadding()
        $separators = if ($hideBorder) { 0 } else { $this._table.Columns.Count - 1 }
        $edges = if ($hideBorder -or !$usePadding) { 0 } else { 2 }
        $padding = 0
        if ($usePadding) {
            foreach ($c in $this._table.Columns) {
                if ($null -ne $c.Padding) { $padding += $c.Padding.GetWidth() }
            }
            if (!$this._padRightCell) {
                $lastCol = $this._table.Columns[$this._table.Columns.Count - 1]
                if ($null -ne $lastCol.Padding) { $padding -= $lastCol.Padding.GetRightSafe() }
            }
        }
        return $separators + $edges + $padding
    }

    [List[int]] CalculateColumnWidths([int]$maxWidth) {
        $widths = [List[int]]::new()
        foreach ($c in $this._table.Columns) {
            $widths.Add($this.MeasureColumn($c, $maxWidth).Max)
        }

        $tableWidth = 0
        foreach ($w in $widths) { $tableWidth += $w }

        if ($tableWidth -gt $maxWidth) {
            $wrappable = [List[bool]]::new()
            foreach ($c in $this._table.Columns) { $wrappable.Add(!$c.NoWrap) }

            $widths = [TableMeasurer]::CollapseWidths($widths, $wrappable, $maxWidth)

            $tableWidth = 0
            foreach ($w in $widths) { $tableWidth += $w }

            if ($tableWidth -gt $maxWidth) {
                $excessWidth = $tableWidth - $maxWidth
                # Very simple reduce: subtract 1 from largest until we meet maxWidth
                while ($excessWidth -gt 0) {
                    $maxW = -1; $maxIdx = -1
                    for ($i=0; $i -lt $widths.Count; $i++) {
                        if ($widths[$i] -gt $maxW) { $maxW = $widths[$i]; $maxIdx = $i }
                    }
                    if ($maxW -le 1) { break }
                    $widths[$maxIdx] -= 1
                    $excessWidth -= 1
                }
            }
        }

        $tableWidth = 0
        foreach ($w in $widths) { $tableWidth += $w }

        if ($tableWidth -lt $maxWidth -and $this._table.Expand) {
            $flexible = [List[bool]]::new()
            $hasFlex = $false
            foreach ($c in $this._table.Columns) {
                $flex = ($null -eq $c.Width)
                $flexible.Add($flex)
                if ($flex) { $hasFlex = $true }
            }

            if ($hasFlex) {
                $excess = $maxWidth - $tableWidth
                while ($excess -gt 0) {
                    # Add to smallest flexible column
                    $minW = 999999; $minIdx = -1
                    for ($i=0; $i -lt $widths.Count; $i++) {
                        if ($flexible[$i] -and $widths[$i] -lt $minW) {
                            $minW = $widths[$i]; $minIdx = $i
                        }
                    }
                    if ($minIdx -eq -1) { break }
                    $widths[$minIdx] += 1
                    $excess -= 1
                }
            }
        }

        return $widths
    }

    [Measurement] MeasureColumn([TableColumn]$column, [int]$maxWidth) {
        if ($null -ne $column.Width) {
            return [Measurement]::new($column.Width, $column.Width)
        }
        $colIdx = $this._table.Columns.IndexOf($column)
        $minWidths = [List[int]]::new()
        $maxWidths = [List[int]]::new()

        $headM = $column.Header.Measure($this._options, $maxWidth)
        $footM = if ($null -ne $column.Footer) { $column.Footer.Measure($this._options, $maxWidth) } else { $headM }

        $minWidths.Add([Math]::Min($headM.Min, $footM.Min))
        $maxWidths.Add([Math]::Max($headM.Max, $footM.Max))

        foreach ($row in $this._table.Rows) {
            $currCol = 0
            foreach ($item in $row.Items) {
                if ($currCol -eq $colIdx) {
                    if ($item -is [TableCell]) {
                        $cSpan = $item.ColumnSpan
                        $cellM = $item.Content.Measure($this._options, $maxWidth)
                        if ($cSpan -gt 1) {
                            $minWidths.Add([Math]::Floor($cellM.Min / $cSpan))
                            $maxWidths.Add([Math]::Floor($cellM.Max / $cSpan))
                        } else {
                            $minWidths.Add($cellM.Min)
                            $maxWidths.Add($cellM.Max)
                        }
                    } else {
                        $rM = $item.Measure($this._options, $maxWidth)
                        $minWidths.Add($rM.Min)
                        $maxWidths.Add($rM.Max)
                    }
                    break
                } elseif ($item -is [TableCell] -and ($currCol + $item.ColumnSpan) -gt $colIdx) {
                    $cSpan = $item.ColumnSpan
                    $cellM = $item.Content.Measure($this._options, $maxWidth)
                    $minWidths.Add([Math]::Floor($cellM.Min / $cSpan))
                    $maxWidths.Add([Math]::Floor($cellM.Max / $cSpan))
                    break
                }
                $currCol += if ($item -is [TableCell]) { $item.ColumnSpan } else { 1 }
            }
        }

        $pad = if ($null -ne $column.Padding) { $column.Padding.GetWidth() } else { 0 }

        $cMin = if ($minWidths.Count -gt 0) { [Linq.Enumerable]::Max([int[]]$minWidths) } else { $pad }
        $cMax = if ($maxWidths.Count -gt 0) { [Linq.Enumerable]::Max([int[]]$maxWidths) } else { $maxWidth }

        return [Measurement]::new($cMin, $cMax)
    }

    static hidden [List[int]] CollapseWidths([List[int]]$widths, [List[bool]]$wrappable, [int]$maxWidth) {
        $totalWidth = 0
        foreach ($w in $widths) { $totalWidth += $w }
        $excessWidth = $totalWidth - $maxWidth

        $hasWrap = $false
        foreach ($w in $wrappable) { if ($w) { $hasWrap = $true; break } }

        if ($hasWrap) {
            while ($totalWidth -ne 0 -and $excessWidth -gt 0) {
                $maxCol = -1
                for ($i=0; $i -lt $widths.Count; $i++) {
                    if ($wrappable[$i] -and $widths[$i] -gt $maxCol) { $maxCol = $widths[$i] }
                }

                $secondMax = -1
                for ($i=0; $i -lt $widths.Count; $i++) {
                    if ($wrappable[$i] -and $widths[$i] -ne $maxCol -and $widths[$i] -gt $secondMax) { $secondMax = $widths[$i] }
                }
                if ($secondMax -eq -1) { $secondMax = 1 }

                $diff = $maxCol - $secondMax

                $ratios = [List[int]]::new()
                $anyRatio = $false
                for ($i=0; $i -lt $widths.Count; $i++) {
                    if ($widths[$i] -eq $maxCol -and $wrappable[$i]) { $ratios.Add(1); $anyRatio = $true } else { $ratios.Add(0) }
                }

                if (!$anyRatio -or $diff -eq 0) { break }

                $toReduce = [Math]::Min($excessWidth, $diff)
                # Apply reduction to max cols
                for ($i=0; $i -lt $widths.Count; $i++) {
                    if ($ratios[$i] -gt 0) { $widths[$i] -= $toReduce }
                }

                $totalWidth = 0
                foreach ($w in $widths) { $totalWidth += $w }
                $excessWidth = $totalWidth - $maxWidth
            }
        }
        return $widths
    }
}