jit-tree.psm1

. $PSScriptRoot\MessageFunctions.ps1

function Write-Tree {
    [CmdletBinding()]
    param (
        # Specifies a path to one or more locations.
        [Parameter(Mandatory = $false, Position = 0, ParameterSetName = "Default",
            ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Path to one or more locations.")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path = (Get-Location),

        [Parameter(Mandatory = $false, ParameterSetName = "Default",
            ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Specifies, as an int, the depth to list, default -1 (all depths).")]
        [int]
        $Depth = -1,

        [Parameter(Mandatory = $false, ParameterSetName = "Default",
            HelpMessage = "Specifies, as a string array, a property or property that this cmdlet excludes from the operation. The value of this parameter qualifies the Path parameter. Enter a path element or pattern, such as *.txt or A*. Wildcard characters are accepted.")]
        [string[]]$Exclude,

        # Parameter help description
        [Parameter(Mandatory = $false, ParameterSetName = "Default",
            HelpMessage = "List files in output")]
        [switch]
        $File,


        [Parameter(Mandatory = $false,
            ParameterSetName = "Default",
            HelpMessage = "Gets files and folders with the specified attributes. This parameter supports all attributes and lets you specify complex combinations of attributes.")]
        [System.Management.Automation.FlagsExpression[System.IO.FileAttributes]]$Attributes,

        [Parameter(Mandatory = $false, ParameterSetName = "Default",
            HelpMessage = "Display filesystem entry metadata.")]
        [switch]$DisplayHint
    )

    # data structure - per pass collect @(depth,bars[],Last/NotLast)
    # [0||Not-Last] ├──.git/
    # [1|0|Last] │ └──refs/
    # [2|0|Not-Last] │ ├──heads/
    # [2|0|Not-Last] │ ├──remotes/
    # [3|0, 2|Last] │ │ └──origin/
    # [2|0|Last] │ └──tags/
    # [3|0|Not-Last] │ ├──jit-psbuild/
    # [3|0|Last] │ └──jit-semver/
    # [0||Last] └──templates/

    function Write-Line($dir, $level, $bars, $isLast, $count, $isDir) {
        $prefix = ""
        for ($i = 0; $i -lt $level; $i++) {
            if ($bars -contains $i) { $prefix += "│ " }
            else { $prefix += " " }
        }

        if ($isLast) { $prefix += "└──" }
        else { $prefix += "├──" }
        $itemMsg = "$prefix$($dir.Name)"
        if ($isDir) {
            $itemMsg += [IO.Path]::DirectorySeparatorChar
        }        

        if ($DebugPreference.value__) {
            $lastMsg = ("Not-Last", "Last")
            $debugMsg = "[$level|$($bars -join ', ')|$($lastMsg[$isLast])] ".PadRight(20, ' ')
        }
        if ($DisplayHint) {
            $hintMsg = "[$($dir.Mode), $($dir.LastWriteTime.ToString("yyyy-MM-dd hh:mm tt")), $($count.ToString().PadLeft(8, ' '))] "
        }

        Write-Output "${debugMsg}${hintMsg}${itemMsg}"
    }

    function GetChildDirectory {

        [OutputType([System.IO.FileSystemInfo[]])]
        param(
            [string]$path
        )
        
        Get-ChildItem $path -Directory -Exclude $Exclude -Attributes $Attributes | Sort-Object -Property Name -Descending
    }

    function GetChildFile {
        
        [OutputType([System.IO.FileSystemInfo[]])]
        param(
            [string]$path
        )

        return Get-ChildItem $path -File -Exclude $Exclude -Attributes $Attributes | Sort-Object -Property Name -Descending
    }

    function GetChildItemCountOrFileLength {
        
        [OutputType([int])]
        param(
            [string]$path
        )

        if ($_.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
            return  $_.GetDirectories().Count 
        }
        return $_.Length  
    }

    function LoadFSItems {

        param (
            [string]$path,
            [int]$level,
            [int[]]$subBar
        )

        $fsitems = @()
        if ($File.IsPresent) {
            $fsitems += GetChildFile -path $path
        }
        $fsitems += GetChildDirectory -path $path
        
        $rtn = @($fsitems | ForEach-Object { 
                @{
                    dir           = $_; 
                    isLast        = $false; 
                    childDirCount = (GetChildItemCountOrFileLength $_); 
                    treeLevel     = $level; 
                    treeBarLevel  = $subBar;
                    isDir         = $_.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
                } })
        
        # set $isLast to true for 1st item (note: decending order)
        if ($rtn.Length -gt 0) {
            $rtn[0].isLast = $true 
        }

        return $rtn
    }

    # Guard path
    if (-Not(Test-Path $Path)) {
        Write-Information -MessageData (Format-ErrorMessage "Not a valid path $Path") -InformationAction Continue
        break
    }
    # root path
    $rootPath = Resolve-Path -Path $Path
    Write-Output $rootPath.Path

    if ($DisplayHint.IsPresent) {
        Write-Output "Columns: [Mode, LastWriteTime, Length, Name]"
    }

    # iterate the tree
    $dir = New-Object -TypeName System.Collections.Stack

    # init root
    LoadFSItems -path $rootPath | ForEach-Object { $dir.Push($_) }

    while ($dir.Count -gt 0) {
        
        $dirEntry = $dir.Pop()

        $currentDir = $dirEntry.dir
        $isLast = $dirEntry.isLast
        $count = $dirEntry.childDirCount
        $level = $dirEntry.treeLevel
        $bars = $dirEntry.treeBarLevel
        $isDir = $dirEntry.isDir

        # adhere to depth
        if (($Depth -ne -1) -and ($level -gt $Depth - 1)) { continue }

        Write-Line -dir $currentDir -level $level -bars $bars -isLast $isLast -count $count -isDir $isDir
        
        if ($isDir) { 
            $subBar = [int[]]$bars
            if (-not($isLast) ) {
                if ($null -eq $subBar) {
                    $subBar = [int[]]($level)
                }
                else {
                    $subBar = $subBar + $level
                }
            }
           
            LoadFSItems -path $currentDir -level ($level + 1) -subBar $subBar | ForEach-Object { $dir.Push($_) }
        }
    }
}

$exportModuleMemberParams = @{
    Function = @(
        'Write-Tree'
    )
    Variable = @()
}

Export-ModuleMember @exportModuleMemberParams