riddlerps.psm1

[cmdletbinding(SupportsShouldProcess=$true)]
param()

function Get-ScriptDirectory
{
    $Invocation = (Get-Variable MyInvocation -Scope 1).Value
    Split-Path $Invocation.MyCommand.Path
}

$script:scriptDir = ((Get-ScriptDirectory) + "\")

$global:riddlerpssettings = New-Object psobject -Property @{    
    MessagePrefix = ' '
    TemplateRoot = (Join-Path ($script:scriptDir) -ChildPath 'templates-v1\Add Project')
    IndentLevel = 1
    WhatIf = $true
    QuitResultKey = 'rps-quit'
}

#######################################################################
# Functions dealing with user input/output
#######################################################################

function Invoke-Prompts{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        $prompts,

        [Parameter(Position=1)]
        [int]$indentLevel=1
    )
    process{
        $private:results = @{}
        
        $requiredPrompts = $prompts | Where-Object { !($_.Type) -or ($_.Type -ne 'optional') }
        $optionalPrompts = $prompts | Where-Object { $_.Type -and ($_.Type -eq 'optional') }

        $requiredPrompts | ForEach-Object {
            $prompt = $_
            $result = Get-PromptResult -prompt $prompt -indentLevel $indentLevel
            foreach($key in $result.Keys){
                if($prompt.PromptType -eq 'PickMany'){
                    $private:results[$key]=$result[$key]
                }
                else{
                    $private:results[$key]=$result[$_.Name]
                }
                
            }
        }

        if($optionalPrompts){
            # optional prompts exist, see if the user want's to answer the questions
            Write-MessagePrefix -indentLevel $indentLevel
            'Show all options?' | Write-Host
            Write-InputPromptText -indentLevel $indentLevel

            $showAllOptoins = ConvertTo-Bool (Get-TextFromUser)
            ' ' | Write-Host

            # loop through optional prompts, only prompt if user accepts.
            $optionalPrompts | ForEach-Object {
                $prompt = $_
                if($showAllOptoins){
                    $result = Get-PromptResult -prompt $prompt -indentLevel $indentLevel
                    foreach($key in $result.Keys){
                        $results[$key]=$result[$_.Name]
                    }
                }
            }
        }

        # apply default values here
        foreach($key in $result.Keys){
            if($result[$key] -eq $null){
                $results[$key]=$prompt.Default
            }
        }

        # return results
        $results
    }
}

function Write-MessagePrefix{
    [cmdletbinding()]
    param(
        [Parameter(Position=0)]
        $indentLevel = 1
    )
    process{
        [string]$prefix=$global:riddlerpssettings.MessagePrefix
        $prefix = ($prefix*$indentLevel)
        $prefix | Write-Host -NoNewline
    }
}

function Write-InputPromptText{
    [cmdletbinding()]
    param(
        [Parameter(Position=0)]
        $indentLevel = 1
    )
    process{
        Write-MessagePrefix -indentLevel $indentLevel
        '>> ' | Write-Host -ForegroundColor Green -NoNewline
    }
}

function Get-PromptResult{
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        $prompt,

        [Parameter(Position=1)]
        $indentLevel = 1,

        $promptForgroundColor = 'Cyan'
    )
    process{
        $name = $prompt.Name
        $result = @{}
        $private:results = @{}

        # display text/options
        Write-MessagePrefix -indentLevel $indentLevel
        "{0}" -f $prompt.Text| Write-Host -ForegroundColor $promptForgroundColor
        $options = $prompt.Options
        $getValFromUser = $true
        if($prompt.Options){
            $optionsType = $options.Type
            if(-not $optionsType){$optionsType = 'Numbered'}

            if($optionsType -eq 'Numbered'){
                $counter = 0
                
                foreach($key in $prompt.Options.Keys){
                    if($key -eq 'Type'){ continue }

                    if( !($options -is [hashtable]) ){
                        if($options -is [array] -and $options.Length -gt 1){
                            $options = $options[1]
                        }
                    }
                    Write-MessagePrefix -indentLevel $indentLevel
                    '{0}={1}' -f $key, $options[$key] | Write-Host
                }                
            }
            else{                
                $optStr = (ConvertTo-OptionsString -options $options)
                $promptResult = Prompt-OptionsString -optionsString $optStr -optionsType $optionsType

                foreach($key in $promptResult.Keys){
                    $results[$key]=$true
                }
                if($optionsType -eq 'PickOne'){
                    $result[$prompt.Name]=($promptResult.Keys | Select-Object -First 1)
                }
                elseif($optionsType -eq 'PickMany'){
                    foreach($key in $promptResult.Keys){
                        $result[$key]=$key
                    }
                }
                $getValFromUser = $false
            }
        }
        
        if($getValFromUser){
            Write-InputPromptText -indentLevel $indentLevel

            # get the value from the user
            if($prompt.PromptAction -is [scriptblock]){
                $valFromUser = (&($prompt.PromptAction))
            }
            else{
                
                if($options){
                    $valFromUser=($options[(Read-Host)])
                }
                else{
                    $valFromUser= Read-Host
                }
            }

            ' ' | Write-Host
            if($valFromUser -eq $null){ $valFromUser=$prompt.Default }

            $result[$prompt.Name]=$valFromUser
        }

        return $result
    }
}

function Write-PromptsSummary {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        $prompts,
        [Parameter(Position=1)]
        $indentLevel = 1
    )
    process{
        Write-MessagePrefix -indentLevel $indentLevel
        # for now just return this so it's displayed directly
        'Prompt results' | Write-Host -ForegroundColor Green
        $prompts <#| Select-Object text,name#>
    }
}

function Get-TextFromUser{
    [cmdletbinding()]
    param()
    process{
        Read-Host
    }
}

function Report-FakeProgress{
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=0)]
        $message,
        [Parameter(Position=1)]
        $totalSeconds = 2
    )
    process{
        $seconds = 0
        $message | Write-Host
        while($seconds -lt $totalSeconds){
            '.........................' | Write-Host -NoNewline -ForegroundColor Green
            Start-Sleep 1
            $seconds++
        }
        "`n" | Write-Host
    }
}

function Get-RelativePath{
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true)]
        [IO.DirectoryInfo]$from,

        [Parameter(Mandatory=$true)]
        [IO.DirectoryInfo]$to
    )
    process{
        Push-Location

        Set-Location $from.FullName

        # compute the relative path
        $relPathToTemplateFile = (Get-Item $to.FullName | Resolve-Path -Relative)
        $relPathToTemplateFile

        Pop-Location
    }
}

function ConvertTo-Bool{
    [cmdletbinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [string]$valueToConvert
    )
    process{
        $truePattern = 'true|t|1|y[es]|y'
        $falsePattern = 'false|f|0|n[o]|n'       

        if($valueToConvert -match $truePattern){
            $valueToReturn = $true   
        }
        elseif($valueToConvert -match $falsePattern){
            $valueToReturn = $false
        }
        elseif(-not $valueToConvert){ <# ignore it#> }
        else{
            throw ('Unknown bool value to convert: [{0}]' -f $valueToConvert)
        }

        $valueToReturn
    }
}

#######################################################################
# Functions relating to displaying/reading optoins
#######################################################################

function ConvertTo-OptionsString{
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        $options
    )
    process{
        $optionsType = $options.Type
        if(-not $optionsType){ $optionsType = 'PickOne' }

        if($optionsType -eq 'PickOne'){
            $delims = @(' (',')')
        }
        else{
            $delims = @(' [',']')
        }
        $str = ''

        foreach($key in $options.Keys){
            if($key -eq 'Type'){continue}

            $str+=( '{0}${2}$ {1} {3}{4}' -f $delims[0], $delims[1],$key,$options[$key],"`n")
        }

        $str
    }
}

function Prompt-OptionsString{
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]
        $optionsString,
        [Parameter(Position=1)]
        $optionsType='PickMany'
    )
    process{
        $parsedOptions = Parse-OptonsString -optionsString $optionsString
        $displayPoints = @()
        # now we have the parsed options

        $str = $parsedOptions.TransformedOptionsString

        $currentIndex = 0
        foreach($m in $parsedOptions.MatchedPoints){
            $str.substring($currentIndex,$m.Position-$currentIndex) | Write-Host -NoNewline
            $currentIndex = $m.Position

            $displayPoints += @{
                Name = $m.Name
                Position=$m.Position
                CursorPosition=$Host.UI.RawUI.CursorPosition
                Value = $null
            }
        }
        $str.substring($currentIndex,$str.length-$currentIndex) | Write-Host -NoNewline
        
        if($optionsType -eq 'PickMany'){
            ' <<select options and press enter>>' | Write-Host -ForegroundColor Gray
        }

        # set cursor to the first match
        $oldPos = $Host.UI.RawUI.CursorPosition
        $Host.UI.RawUI.CursorPosition = $displayPoints[0].CursorPosition
        $continueLoop = $true
        $boundaries = @{
            X = @{
                Min = ($displayPoints.CursorPosition.X | Measure-Object -Minimum).Minimum
                Max = ($displayPoints.CursorPosition.X | Measure-Object -Maximum).Maximum
            }
            Y = @{
                Min = ($displayPoints.CursorPosition.Y | Measure-Object -Minimum).Minimum
                Max = ($displayPoints.CursorPosition.Y | Measure-Object -Maximum).Maximum
            }
        }
        
        $oldCursorSize = $Host.UI.RawUI.CursorSize
        $oldFgColor = $Host.ui.RawUI.ForegroundColor
        $Host.UI.RawUI.ForegroundColor = 'Yellow'
        $results = @{}
        if($GitPromptSettings){
            $GitPromptSettings.DefaultForegroundColor = $Host.UI.RawUI.ForegroundColor
        }
        while($continueloop){
            $Host.UI.RawUI.CursorSize = 100
            $key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
            $pos = $Host.UI.RawUI.CursorPosition
            if( @(81,27).Contains($key.VirtualKeyCode) ){
                # q, Esc
                $continueloop = $false
                $results[($global:riddlerpssettings.QuitResultKey)]=$true
                break
            }
            elseif($optionsType -eq 'PickMany' -and $key.VirtualKeyCode -eq 13){
                # Enter key for PickMany
                $continueloop = $false
                break
            }
            elseif($key.VirtualKeyCode -eq 38){
                # Up arrow key
                if(([int]($pos.Y)-1) -ge $boundaries.Y.Min){
                    $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates -ArgumentList @($pos.X,([int]($pos.Y)-1))    
                }                
            }
            elseif($key.VirtualKeyCode -eq 40){
                # down arrow
                if(([int]($pos.Y)+1) -le $boundaries.Y.Max){
                    $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates -ArgumentList @($pos.X,([int]($pos.Y)+1))
                }
            }
            elseif(
                ($key.VirtualKeycode -ge 48 -and $key.VirtualKeyCode -le 90) -or 
                ($key.VirtualKeyCode -eq 32) -or 
                ($key.VirtualKeyCode -eq 13)){
                # any key 0-9 or a-z; spacebar (32); Enter (13)
                $nowPos = $Host.UI.RawUI.CursorPosition
                $setValue = $false
                foreach($p in $displayPoints){
                    $cPos = $p.CursorPosition
                    if($cPos.X -eq $nowPos.X -and $cPos.Y -eq $nowPos.Y){
                        # toogle the value of the text
                        $rect = New-Object 'System.Management.Automation.Host.Rectangle' -ArgumentList @($cPos.x,$cPos.y,$cPos.x,$cPos.y)
                        $currentChar = $Host.UI.RawUI.GetBufferContents($rect).Character
                        if($currentChar -eq 'X'){
                            ' ' | Write-Host
                        }
                        else{
                            'X' | Write-Host
                            $setValue = $true
                            if($optionsType -eq 'PickOne'){
                                $continueLoop = $false
                            }
                        }
                    }
                }

                if($optionsType -eq 'PickOne' -and $setValue){
                    $tpos = $Host.UI.RawUI.CursorPosition
                    foreach($p in $displayPoints){
                        $cPos = $p.CursorPosition
                        if(-not ($cPos.X -eq $nowPos.X -and $cPos.Y -eq $nowPos.Y)){
                            $Host.UI.RawUI.CursorPosition=New-Object System.Management.Automation.Host.Coordinates -ArgumentList @($cPos.X,$cPos.Y)
                            ' ' | Write-Host
                        }
                    }
                    $Host.UI.RawUI.CursorPosition = $tpos             
                }

                $Host.UI.RawUI.CursorPosition = $nowPos
            }
            else{
                # unknown key, just ignore it
            }
        }

        $Host.UI.RawUI.ForegroundColor = $oldFgColor
        if($GitPromptSettings){
            $GitPromptSettings.DefaultForegroundColor = $Host.UI.RawUI.ForegroundColor
        }

        $Host.UI.RawUI.CursorSize = $oldCursorSize
        $Host.UI.RawUI.CursorPosition = $oldPos
        
        # now loop through display points and see if they have some text
        foreach($displayPoint in $displayPoints){        
            $p = $displayPoint.CursorPosition
            [int]$x = $p.x
            [int]$y = $p.y
            $rect = New-Object 'System.Management.Automation.Host.Rectangle' -ArgumentList @($x,$y,$x,$y)
            $displayPoint.Value = $host.ui.RawUI.GetBufferContents($rect).Character
        }
        
        $selectedOptions=@()
        $displayPoints | Where-Object {$_.Value -and $_.Value -ne ' '} | ForEach-Object{
            $results[$_.Name]=$_.Value

            # for some reason if I take this out it doesn't work any longer?
            ' ' |Write-Host
            $selectedOptions += $_.Name
        }
        'results: [{0}]' -f ($results.Keys -join ';') | Write-Verbose
        $results
    }
}

function New-PromptObject{
    [cmdletbinding()]
    param(
        [Parameter(Position=0,ValueFromPipelineByPropertyName =$true)]
        $name = 'userprompt',

        [Parameter(Position=1,Mandatory=$true,ValueFromPipelineByPropertyName =$true)]
        $text,

        [Parameter(Position=2,ValueFromPipelineByPropertyName =$true)]
        [ValidateSet("Question","PickOne","PickMany",'Bool',"Ordered")]
        $promptType = "Question",

        [Parameter(Position=3,ValueFromPipelineByPropertyName =$true)]
        $options,

        [Parameter(Position=4,ValueFromPipelineByPropertyName=$true)]
        [ScriptBlock]$promptAction,
        $defaultValue,

        [Parameter(Position=5,ValueFromPipelineByPropertyName=$true)]
        [bool]$optional = $false
    )
    process{
        
        # Convert Bool type to a PickOne
        if($promptType -eq 'Bool'){
            $promptAction = ({ ConvertTo-Bool(Get-TextFromUser) })

            $promptType = 'PickOne'

            if(-not $defaultValue){
                $defaultValue = 'no'
            }

            $options = ([ordered]@{
                            'yes'='Yes'
                            'no'='No'
                        })
        }

        # we need to add the type to the options object
        if($options -is [hashtable] -or
            $options -is [System.Collections.Specialized.OrderedDictionary]){
                $options['type']=$promptType
            }

        $typeToAdd = $null
        
        New-Object psobject -Property @{
            Name = $name
            Text = $text
            Default = $defaultValue
            Options = $options
            PromptType = $promptType
            PromptAction=$promptAction
            Type = $typeToAdd
        }
    }
}

function Parse-OptonsString{
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        $optionsString
    )
    process{
        $matchedPoints = @()
        $newOptionsStr = ''
        $regex=[regex] '(?<options>\$[^\$]+\$)'

        $currentIndex = 0
        $allMatches = $regex.Matches($optionsString)
        foreach($match in $allMatches){
            $group = $match.Groups['options']
            if($group.Success){
                $length = $group.Index + $group.Length - $currentIndex

                $newOptionsStr+=$optionsString.Substring($currentIndex,$group.Index - $currentIndex)
                $matchedPoints += @{
                    Name = $group.value.substring(1,$group.value.length-2)
                    Position = $newOptionsStr.Length
                }
                $currentIndex += $length
            }
        }

        $newOptionsStr += $optionsString.substring($currentIndex,$optionsString.length - $currentIndex)
        @{
            OptionsString = $optionsString
            TransformedOptionsString = $newOptionsStr
            MatchedPoints = $matchedPoints
        }
    }
}