Private/Console/Tree.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 .\Internal.psm1
using module .\Widgets.psm1

class TreeGuide {
    [TreeGuide] get_SafeTreeGuide() { return $this }
    [string] GetPart([TreeGuidePart]$part) { return '' }

    static [TreeGuide] get_Line() { return [LineTreeGuide]::new() }
    static [TreeGuide] get_BoldLine() { return [BoldLineTreeGuide]::new() }
    static [TreeGuide] get_DoubleLine() { return [DoubleLineTreeGuide]::new() }
    static [TreeGuide] get_Ascii() { return [AsciiTreeGuide]::new() }

    static [TreeGuide] GetSafeTreeGuide([RenderOptions]$options, [TreeGuide]$guide) {
        if ($null -eq $guide) { return [TreeGuide]::get_Line() }
        if (!$options.Unicode) { return $guide.get_SafeTreeGuide() }
        return $guide
    }
}

class LineTreeGuide : TreeGuide {
    [TreeGuide] get_SafeTreeGuide() { return [AsciiTreeGuide]::new() }
    [string] GetPart([TreeGuidePart]$part) {
        switch ($part) {
            ([TreeGuidePart]::Space) { return ' ' }
            ([TreeGuidePart]::Continue) { return '│ ' }
            ([TreeGuidePart]::Fork) { return '├── ' }
            ([TreeGuidePart]::End) { return '└── ' }
        }
        return ''
    }
}

class BoldLineTreeGuide : TreeGuide {
    [TreeGuide] get_SafeTreeGuide() { return [AsciiTreeGuide]::new() }
    [string] GetPart([TreeGuidePart]$part) {
        switch ($part) {
            ([TreeGuidePart]::Space) { return ' ' }
            ([TreeGuidePart]::Continue) { return '┃ ' }
            ([TreeGuidePart]::Fork) { return '┣━━ ' }
            ([TreeGuidePart]::End) { return '┗━━ ' }
        }
        return ''
    }
}

class DoubleLineTreeGuide : TreeGuide {
    [TreeGuide] get_SafeTreeGuide() { return [AsciiTreeGuide]::new() }
    [string] GetPart([TreeGuidePart]$part) {
        switch ($part) {
            ([TreeGuidePart]::Space) { return ' ' }
            ([TreeGuidePart]::Continue) { return '║ ' }
            ([TreeGuidePart]::Fork) { return '╠══ ' }
            ([TreeGuidePart]::End) { return '╚══ ' }
        }
        return ''
    }
}

class AsciiTreeGuide : TreeGuide {
    [string] GetPart([TreeGuidePart]$part) {
        switch ($part) {
            ([TreeGuidePart]::Space) { return ' ' }
            ([TreeGuidePart]::Continue) { return '| ' }
            ([TreeGuidePart]::Fork) { return '|-- ' }
            ([TreeGuidePart]::End) { return '`-- ' }
        }
        return ''
    }
}

class TreeNode {
    [IRenderable]$Renderable
    [List[TreeNode]]$Nodes
    [bool]$Expanded

    TreeNode([IRenderable]$renderable) {
        $this.Renderable = $renderable
        $this.Nodes = [List[TreeNode]]::new()
        $this.Expanded = $true
    }

    [TreeNode] AddNode([IRenderable]$renderable) {
        $node = [TreeNode]::new($renderable)
        $this.Nodes.Add($node)
        return $node
    }

    [TreeNode] AddNode([string]$text) {
        return $this.AddNode([Markup]::new($text))
    }
}

class Tree : IRenderable {
    hidden [TreeNode]$_root
    [TreeNode]$Root
    [TreeGuide]$Guide
    [Style]$Style

    Tree([IRenderable]$renderable) {
        $this._root = [TreeNode]::new($renderable)
        $this.Root = $this._root
        $this.Guide = [TreeGuide]::get_Line()
        $this.Style = [Style]::Plain
    }

    Tree([string]$label) {
        $this._root = [TreeNode]::new([Markup]::new($label))
        $this.Root = $this._root
        $this.Guide = [TreeGuide]::get_Line()
        $this.Style = [Style]::Plain
    }

    [List[TreeNode]] get_Nodes() { return $this._root.Nodes }

    [TreeNode] AddNode([IRenderable]$renderable) {
        return $this._root.AddNode($renderable)
    }

    [TreeNode] AddNode([string]$text) {
        return $this._root.AddNode($text)
    }

    [Measurement] Measure([RenderOptions]$options, [int]$maxWidth) {
        $safeGuide = [TreeGuide]::GetSafeTreeGuide($options, $this.Guide)
        $guideWidth = [Cell]::GetCellLength($safeGuide.GetPart([TreeGuidePart]::End))
        $measurements = [List[Measurement]]::new()
        $this.CollectMeasurements($this._root, $measurements, 0, $guideWidth, $options, $maxWidth)

        if ($measurements.Count -eq 0) {
            return [Measurement]::new(0, 0)
        }

        $min = 0
        $max = 0
        foreach ($measurement in $measurements) {
            if ($measurement.Min -gt $min) { $min = $measurement.Min }
            if ($measurement.Max -gt $max) { $max = $measurement.Max }
        }

        return [Measurement]::new([Math]::Min($min, $maxWidth), [Math]::Min($max, $maxWidth))
    }

    [object[]] Render([RenderOptions]$options, [int]$maxWidth) {
        $result = [List[Segment]]::new()
        $visited = [HashSet[int]]::new()
        $this.RenderNode($this._root, $options, $maxWidth, [string[]]@(), $true, $visited, $result)

        while ($result.Count -gt 0 -and $result[$result.Count - 1].IsLineBreak) {
            $result.RemoveAt($result.Count - 1)
        }

        return $result.ToArray()
    }

    hidden [void] CollectMeasurements([TreeNode]$node, [List[Measurement]]$measurements, [int]$depth, [int]$guideWidth, [RenderOptions]$options, [int]$maxWidth) {
        if ($null -eq $node) {
            return
        }

        $prefixWidth = $depth * $guideWidth
        $availableWidth = [Math]::Max(1, $maxWidth - $prefixWidth)
        $measurement = $node.Renderable.Measure($options, $availableWidth)
        $measurements.Add([Measurement]::new($measurement.Min + $prefixWidth, $measurement.Max + $prefixWidth))

        if ($node.Expanded) {
            foreach ($child in $node.Nodes) {
                $this.CollectMeasurements($child, $measurements, $depth + 1, $guideWidth, $options, $maxWidth)
            }
        }
    }

    hidden [void] RenderNode([TreeNode]$node, [RenderOptions]$options, [int]$maxWidth, [string[]]$prefixParts, [bool]$isLast, [HashSet[int]]$visited, [List[Segment]]$result) {
        if ($null -eq $node) {
            return
        }

        $nodeId = [Runtime.CompilerServices.RuntimeHelpers]::GetHashCode($node)
        if (!$visited.Add($nodeId)) {
            throw [InvalidOperationException]::new('Cycle detected in tree.')
        }

        $prefix = [List[Segment]]::new()
        foreach ($part in $prefixParts) {
            $prefix.Add([Segment]::new($part, $this.Style))
        }

        $prefixWidth = [Segment]::CellCount($prefix)
        $availableWidth = [Math]::Max(1, $maxWidth - $prefixWidth)
        $lines = [Segment]::SplitLines($node.Renderable.Render($options, $availableWidth), $availableWidth)
        if ($lines.Count -eq 0) {
            $lines = [List[SegmentLine]]::new()
            $lines.Add([SegmentLine]::new())
        }

        for ($lineIndex = 0; $lineIndex -lt $lines.Count; $lineIndex++) {
            if ($prefix.Count -gt 0) {
                $result.AddRange($prefix)
            }

            $result.AddRange($lines[$lineIndex].Segments)
            $result.Add([Segment]::LineBreak)

            if ($lineIndex -eq 0 -and $prefixParts.Length -gt 0) {
                $continuationParts = [string[]]$prefixParts.Clone()
                $continuationParts[$continuationParts.Length - 1] = $this.GetGuide($options, $isLast ? [TreeGuidePart]::Space : [TreeGuidePart]::Continue).Text
                $prefix = [List[Segment]]::new()
                foreach ($part in $continuationParts) {
                    $prefix.Add([Segment]::new($part, $this.Style))
                }
            }
        }

        if (!$node.Expanded -or $node.Nodes.Count -eq 0) {
            return
        }

        for ($index = 0; $index -lt $node.Nodes.Count; $index++) {
            $child = $node.Nodes[$index]
            $childIsLast = ($index -eq $node.Nodes.Count - 1)
            $childPrefix = [List[string]]::new()
            $childPrefix.AddRange($prefixParts)
            if ($prefixParts.Length -gt 0) {
                $childPrefix[$childPrefix.Count - 1] = $this.GetGuide($options, $isLast ? [TreeGuidePart]::Space : [TreeGuidePart]::Continue).Text
            }
            $childPrefix.Add($this.GetGuide($options, $childIsLast ? [TreeGuidePart]::End : [TreeGuidePart]::Fork).Text)
            $this.RenderNode($child, $options, $maxWidth, $childPrefix.ToArray(), $childIsLast, $visited, $result)
        }
    }

    hidden [Segment] GetGuide([RenderOptions]$options, [TreeGuidePart]$part) {
        $safeGuide = [TreeGuide]::GetSafeTreeGuide($options, $this.Guide)
        $styleToUse = if ($null -ne $this.Style) { $this.Style } else { [Style]::Plain }
        return [Segment]::new($safeGuide.GetPart($part), $styleToUse)
    }
}