experimental/KubeHelpParser.psm1

$CommandTopicTokens = @("Commands.*:$")
$UsageTopicToken = "Usage:"
$ExampleToken = "Examples:"
$OptionToken = "Options:"
$SubCommandTokens   = @("Available Commands", "Basic Commands \(Beginner\):", "Basic Commands \(Intermediate\):",
    "Deploy Commands:", "Cluster Management Commands:", "Troubleshooting and Debugging Commands:",
    "Advanced Commands:", "Settings Commands:", "Other Commands:" )
$SubCommandTokenPattern = $script:SubCommandTokens -join "|"
$IAM = "${PSScriptRoot}\KHP2.psm1"

[string[]]$allTokens = @($CommandTopicTokens, $UsageTopicToken, $ExampleToken, $OptionToken)
[System.Globalization.TextInfo]$TextInfo = [CultureInfo]::new("en-us",$false).TextInfo


class UsageInfo {
    [string]$Usage
    [bool]$supportsFlags
    [bool]$hasOptions
    hidden [string[]]$originalText
    UsageInfo([string[]]$text) {
        for ( $i = 0; $i -lt $text.Count; $i++ ) {
            if ( $text[$i] -match $script:UsageTopicToken ) {
                $i++
                while ( $text[$i] -ne "" ) {
                    $this.originalText += $text[$i]
                    $i++
                }
                break
            }
        }
        $this.Usage = ($this.originalText -join [environment]::newline).Trim()
        if ( $this.Usage -match "\[flags\]") { $this.supportsFlags = $true }
        if ( $this.Usage -match "\[options\]") { $this.hasOptions = $true }
    }
    [string]ToString() {
        return $this.Usage
    }
}

class ExampleInfo {
    [string]$Description
    [string]$Command
    ExampleInfo([string[]]$text) {
        foreach ( $line in $text ) {
            if ( "${line}".Trim() -match "^#" ) {
                $this.Description += "${line}".Trim(" #")
            }
            else {
                $this.Command += "${line}".Trim()
            }
        }
    }
    static [ExampleInfo[]]GetExamples([string[]]$text) {
        [ExampleInfo[]]$examples = @()
        $getExamples = $false
        for ( $i = 0; $i -lt $text.Length; $i++) {
            if ( $text[$i] -match "^Examples:" ) {
                $getExamples = $true
                continue
            }
            if ( $getExamples ) {
                if ( $text[$i][0] -match "^[A-Z]" ) {
                    break
                }
                if ( $text[$i].Length -eq 0 ) {
                    continue
                }
                if ( $text[$i].Trim() -match "^#" ) {
                    $examples += [ExampleInfo]::new($text[$i..++$i])
                }
            }
        }
        return $examples
    }
    [string]ToString() {
        return $this.Command
    }
}


# note that for PowerShell, we won't have parameter aliases
# An option takes the shape of '--<name>=<defaultvalue>: <description>"
# it can also look like '-<n>, --<name>=<defaultvalue>: <description>" where '-<n>' is an option alias
# we through away the aliases
# The <defaultvalue> might be something that we can interpret, so try
# also, some options can be converted to powershell switches (their default value is True or False).
# if the default value is 'True',
# convert that to "No<name>" when building the string which represents the option
class ParameterInfo {
    # we need to track the original name of the parameter
    [string]$OriginalParameterName
    [string]$Name
    [string]$Description
    [object]$DefaultValue
    [type]$ValueType
    [bool]$IsMandatory
    hidden [bool]$Parsed
    hidden [string]$originalText
    ParameterInfo ([string]$text, [bool]$isMandatory = $false) {
        $this.originalText = $text
        if ( $text -match ".* --(?<option>.[^ ]*): (?<Description>.*)" ) {
            $pname,$default = $matches['option'] -split "="
            $this.OriginalParameterName = "--${pname}"
            $pDefaultValue = $default.Trim("'")
            # $this.Name = "${pname}" # .Trim() -replace "-(.)",{($_ -replace "-").ToUpper()} -replace "^(.)",{"$_".ToUpper()}
            # strip away all the "-" and capitalize the first letter of every "word"
            $this.Name = ("${pname}" -split "-").foreach({${script:TextInfo}.ToTitleCase($_)}) -join ""
            if ( $this.Name -eq "DryRun" ) { $this.Name = "WhatIf" }
            $this.Description = $matches['Description'].Trim()
            $this.IsMandatory = $isMandatory
            $this.Parsed = $true
            $v = $null
            if ( [string]::isnullOrEmpty($pDefaultValue)) {
                $this.ValueType = [string]
            }
            elseif ( [int]::TryParse($pDefaultValue, [ref]$v)) {
                $this.ValueType = [int]
                $pDefaultValue = $v
            }
            elseif ( [double]::TryParse($pDefaultValue, [ref]$v)) {
                $this.ValueType = [double]
                $pDefaultValue = $v
            }
            elseif ( $pDefaultValue -eq '[]' ) {
                $this.ValueType = [array]
                $pDefaultValue = @()
            }
            elseif ( $pDefaultValue -eq 'true' -or $pDefaultValue -eq 'false' ) {
                $this.ValueType = [bool]
                $pDefaultValue = [bool]::Parse($pDefaultValue)
            }
            else {
                $this.ValueType = [string]
            }
            $this.DefaultValue = $pDefaultValue
        }
        else {
            Write-Warning "Could not convert '$text' into a parameter"
        }
    }

    static [ParameterInfo[]]GetParameters([string[]]$text) {
        [ParameterInfo[]]$p = @()
        for($i = 0; $i -lt $text.Count; $i++) {
            if ( $text[$i] -match "^Options:" ) {
                $i++
                do {
                    if ( $i -ge $text.Count ) { break }
                    $p += [parameterinfo]::new($text[$i], $false)
                } while ( $text[++$i] -ne "" )
            }
        }
        return $p
    } 

    # this takes the usage text and retrieves the parameters which are not options or flags
    # they are mandatory
    static [ParameterInfo[]]GetMandatoryParameters([string]$text) {
        [parameterinfo[]]$mp = @()
        return $mp
    }

    [string]ToString() {
        # this is a bool, we convert it to a switch parameter
        if ( $this.ValueType -eq [bool] ) {
            $pName = $this.Name
            if ( $this.DefaultValue ) {
                $pName = "No${pName}"
            }
            $pString = '[Parameter(Mandatory=${0})][switch]${{{1}}}' -f $this.IsMandatory,$pName
        }
        elseif ( $this.ValueType -eq [array] -and ! $this.DefaultValue ) {
                $pString = '[Parameter(Mandatory=${0})][{1}]${{{2}}} = @()' -f $this.IsMandatory,$this.ValueType,$this.Name
        }
        elseif ( $this.ValueType -eq [string] -and $this.DefaultValue ) {
                $pString = '[Parameter(Mandatory=${0})][{1}]${{{2}}} = "{3}"' -f $this.IsMandatory,$this.ValueType,$this.Name,$this.DefaultValue
            
        }
        elseif ( $this.DefaultValue ) {
                $pString = '[Parameter(Mandatory=${0})][{1}]${{{2}}} = {3}' -f $this.IsMandatory,$this.ValueType,$this.Name,$this.DefaultValue
        }
        else {
            $pString = '[Parameter(Mandatory=${0})][{1}]${{{2}}}' -f $this.IsMandatory,$this.ValueType,$this.Name
        }
        return $pString
    }
}

# general options for kubectl are a little different. The tag does not exist, so we will force it
class KubeGeneralOptions {
    static [ParameterInfo[]]$Parameters
    static KubeGeneralOptions() {
        $text = Invoke-Kubectl options | Where-Object { "$_" }
        $text[0] = "Options:"
        [KubeGeneralOptions]::Parameters = [ParameterInfo]::GetParameters($text)
    }
}

class Command {
    [string]$Command
    [string[]]$CommandElements
    [string]$Description
    [UsageInfo]$Usage
    [Command[]]$SubCommands
    [ParameterInfo[]]$Parameters
    [ParameterInfo[]]$MandatoryParameters
    [ExampleInfo[]]$Examples
    hidden [string[]]$originalText

    # don't check the MandatoryParameters, or the CommonParameters
    [bool]SupportsWhatIf() {
        return [bool]$this.Parameters.Where({$_.Name -eq "WhatIf"})
    }

    Command ([string[]]$command, [string[]]$text ) {
        $this.Command = ($command -join " ").Trim()
        $this.CommandElements = $command # "$command".Trim()
        $this.originalText = $text
        #$c,$d = "$text".Trim().Split(" ", 2, [System.StringSplitOptions]::RemoveEmptyEntries) | ForEach-Object {"$_".Trim()}
        #$this.Command = $c
        $this.Description = [Command]::GetDescription($text).Trim()
        $this.Parameters = [ParameterInfo]::GetParameters($text)
        $this.Examples = [ExampleInfo]::GetExamples($text)
        $this.Usage = [UsageInfo]::new($text)
        $this.MandatoryParameters = [ParameterInfo]::GetMandatoryParameters($this.Usage.Usage)
        $this.SubCommands = $this.GetSubCommands($command, $text)
    }

    [Command[]]GetSubCommands([string[]]$commandElements, [string[]]$text) {
        [Command[]]$subCmds = @()
        #$jobs = @()
        #$jobResult = @()
        #$pattern = $script:SubCommandTokenPattern
        for ($i = 0; $i -lt $text.count; $i++ ) {
            if ( $text[$i] -match $script:SubCommandTokenPattern ) {
                $i++
                while ( $i -lt $text.count -and $text[$i][0] -eq " " ) {
                    $subcmd,$desc = $text[$i].Trim().split(" ",2, [System.StringSplitOptions]::RemoveEmptyEntries)
                    # we need to add help specifically here, since we're invoking kubectl directly
                    $elements = $commandElements + @($subcmd)
                    $subText = Invoke-Kubectl ($elements + "--help")
# $sc = [Command]::new($subcmd, $subText)
                    $sc = [Command]::new($elements, $subText)
                    $sc.CommandElements = $elements
                    $subCmds += $sc
        ######
        # ATTEMPT AT JOBS
        ######
        # $cmdText = $text[$i]
        # $jobs += Start-ThreadJob {
        # $t = ${using:cmdText}
        # $subcmd,$desc = $t.Trim().split(" ", 2, [System.StringSplitOptions]::RemoveEmptyEntries)
        # $elements = $using:commandElements + @($subcmd) + "--help"
        # $subText = kubectl $elements
        # @{ Command = $subcmd; Description = $desc; Elements = $elements; Text = $subText }
        # #$sc = [Command]::new($subcmd, $subText)
        # #$sc.CommandElements = $elements
        # #$sc
        # }
                    $i++
                }
            }
        }
        #if ( $jobs.Count -gt 0 ) {
        # Write-Host ($jobs.Count)
        # wait-job $local:jobs
        # $local:jobResult = Receive-Job $local:jobs
        # foreach ( $c in $local:jobResult ) {
        # if ( ! $c.Command ) {
        # "whoops"
        # }
        # if ( ! $c.Elements ) {
        # "whoops"
        # }
        # $sc = [Command]::new($c.Command, $c.Text)
        # $sc.Elements = $c.Elements
        # $subCmds += $sc
        # }
        #}
        return $subCmds
    }

    [Command[]]GetAllCommands() {
        [Command[]]$cmds = @()
        $cmds += $this
        $cmds += $this.SubCommands.Foreach({$_.GetAllCommands()})
        return $cmds
    }

    [Command[]]GetBranchCommands() {
        [Command[]]$cmds = @()
        if ( $this.SubCommands.Count -gt 0 ) {
            $this.SubCommands.Foreach({$_.GetBranchCommands()})
            $cmds += $this.SubCommands
        }
        return $cmds

    }
    [Command[]]GetLeafCommands() {
        [Command[]]$cmds = @()
        if ( $this.SubCommands.Count -gt 0 ) {
            $cmds += $this.SubCommands.Foreach({$_.GetLeafCommands()})
        }
        else {
            $cmds += $this
        }
        return $cmds
    }

    # not all commands support -o json. We need a way to add it or not when we execute
    # the only way we can know is by going through the options
    [bool]SupportsJsonOutput() {
        return ($null -ne $this.Parameters.Where({$_.OriginalParameterName -eq "--output" -and $_.Description -match "json"}))
    }

    [string]CreateParamStatement() {
        $sb = [System.Text.StringBuilder]::new()
        $param = @()
        $sb.AppendLine("param (")
        $this.MandatoryParameters.Foreach({$param += $_.ToString()})
        $this.Parameters.Foreach({$param += $_.ToString()})
        $sb.AppendLine(($param -join ",`n")) # comma separate the parameters
        $sb.AppendLine(")")
        return $sb.ToString()
    }

    [string]CreateCommentBasedHelp() {
        $sb = [System.Text.StringBuilder]::new()
        $sb.AppendLine("<#")
        $sb.AppendLine(".SYNOPSIS")
        $sb.AppendLine($this.Description.Split("`n")[0])
        $sb.AppendLine()
        $sb.AppendLine(".DESCRIPTION")
        $sb.AppendLine($this.Description)
        $sb.AppendLine("The native usage for this command is:")
        $sb.AppendLine(" " + $this.Usage.Usage)
        $sb.AppendLine()
        foreach ( $p in $this.MandatoryParameters ) {
            $sb.AppendLine(".PARAMETER")
            $sb.AppendLine($p.Name)
            $sb.AppendLine($p.Description)
            $sb.AppendLine("The original kubenetes parameter is {0}" -f $p.OriginalParameterName)
            $sb.AppendLine()
        }
        foreach ( $p in $this.Parameters ) {
            $sb.Append(".PARAMETER ")
            $sb.AppendLine($p.Name)
            $sb.AppendLine($p.Description)
            $sb.AppendLine()
        }
        foreach ( $e in $this.Examples ) {
            $sb.AppendLine(".EXAMPLE")
            $sb.AppendLine('$ ' + $e.Command)
            $sb.AppendLine($e.Description)
            $sb.AppendLine()
        }
        $sb.AppendLine("#>")
        return $sb.ToString()
    }

    static [string]GetDescription([string[]]$text) {
        [string[]]$dText = ""
        for($i = 0; $i -lt $text.Count; $i++ ) {
            if ( $script:allTokens | Where-Object { $text[$i] -match $_ } ) {
                break
            }
            $dText += $text[$i]
        }
        return ($dText -join [Environment]::newline)
    }

    [string]CreateProxyFunction() {
        [string[]]$s = .{
            'function Invoke-Kube{0}' -f ($this.CommandElements -join "")
            '{'
            '[CmdletBinding()]'
            $this.CreateParamStatement()
            $this.GetParameterMap()
            '$commandArguments = $PSBoundParameters.Keys.Foreach({"{0} ''{1}''" -f $_parameterMap[$_],$PSBoundParameters[$_]})'
            'if ( $env:DEBUGPROXYFUNCTION -eq 1 ) { wait-debugger }'
            if ( $this.SupportsJsonOutput ) {
                'kubectl $commandArguments -o json'
            }
            else {
                'kubectl $commandArguments'
            }
            '}'
        }
        wait-debugger
        return ($s -join [environment]::NewLine)
    }

    # this captures the information about the parameters in a .psd block.
    # this is needed when we want to take the parameters that the user provided and turn them back into the
    # strings we need when we call the actual command
    [string]GetParameterMap() {
        [string[]]$parameterStrings = '$_parameterMap = @{ '
        foreach ( $parameter in $this.Parameters ) {
            $parameterStrings += "'{0}' = '{1}'" -f $parameter.Name,$parameter.OriginalParameterName
        }
        $parameterStrings += "}"
        return ($parameterStrings -join [Environment]::NewLine)
    }

    [string]ToString() {
        return "kubectl " + ($this.CommandElements -join " ")
    }
}
# invoke kubectl and log it
function Invoke-Kubectl {
    [CmdletBinding()]
    param ( [Parameter(ValueFromRemainingArguments,Position=0)][string[]]$command )
    $p = "kubectl $($command -join ' ')"
    # show-prog -message "$p"
    write-verbose -verb "$p"
    $text = kubectl $command
    if ( ! $text ) {
        Wait-Debugger
    }
    $text
}

function Get-CommandInfo ( [string[]]$commandElements ) {
    $ce = $commandElements
    $ce += "--help"
    $text = Invoke-Kubectl -command $ce
    if ( $commandElements.Count -gt 0 ) {
        $cmd = $commandElements[0]
    }
    else {
        $cmd = ""
    }
    [Command]::new($cmd, $text)
}
function Get-KubeCtlGeneralOptions
{
    [KubeGeneralOptions]::Parameters
}

function Get-KubeCommands ()
{
    $cmd = Get-CommandInfo
    $cmd
}