CacheMeIfYouCan.psm1

using namespace System.Collections.Generic
using namespace System.Collections
using namespace System.Management.Automation.Language
using namespace System.Management.Automation
using namespace System.Management

# ModuleConfig
# ExportCoercionFunctions
$script:DefaultTemplateTypeMapping = @{}
$script:ModuleConfig = @{
    ExportAggressiveNames = $false
    VerboseLifetimeMessages = $true
    ExportDebugFunctions = $false
    VerboseJson_ArgCompletions = $false
    PrintExtraSummaryOnTabCompletion = $false
    WarnWhen_GetValueCommand_KeyDoesNotExist = $true
}


$script:Cache = @{}
if($Script:ModuleConfig.VerboseJson_ArgCompletions) {
    (Join-Path (gi 'temp:\') 'CacheMeIfYouCan.ArgCompletions.log')
    | Join-String -op 'CacheMeIfYouCan: VerboseLogging for ArgCompletions is enabled at: '
    | write-warning
}


$script:Color = @{
    DimYellow = '#dcdcaa'
    DimOrange = '#ce8d70'
    DimPurple = '#c586c0'
}

function Cache._Read {
    <#
    .SYNOPSIS
        Internal helper, that encapsulates reading keys
 
    Why the name? I wrote it during an 8 hour car ride.
    #>

    # [Alias('Cache._Read')]
    [OutputType( [CachedRecord] )]
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory)]
            [Alias('Name')]
            [ArgumentCompleter( [CachedKeyNameArgumentCompleter] )]
            [string]$KeyName,

            [switch]$ErrorWhenMissing
    )



    [bool]$exists? = $script:Cache.ContainsKey( $KeyName )
    if( $ErrorWhenMissing -and -not $exists? ) {
        if($script:ModuleConfig.WarnWhen_GetValueCommand_KeyDoesNotExist) {
            $PSCmdlet.MyInvocation.ScriptName
                | Join-String -op 'Cmdlet.MyInvo.Script: '
                | Write-Debug
            throw ( $KeyName | Join-String -f 'CacheMe::_Read: Tried reading from a key that does not exist! Key = "{0}"')
        } else {
            $KeyName
                | Join-String -f 'CacheMe::_Read: Tried reading from a key that does not exist! Key = "{0}"'
                | Write-Debug
        }

    }
    return $script:Cache[ $KeyName ]
}

# class CachedKeyNameArgumentCompleter : IArgum
class CachedKeyNameArgumentCompleter : System.Management.Automation.IArgumentCompleter {
    [IEnumerable[CompletionResult]] CompleteArgument(
        [string] $CommandName,
        [string] $ParameterName,
        [string] $WordToComplete,
        [CommandAst] $CommandAst,
        [IDictionary] $FakeBoundParameters
    ) {
        [List[CompletionResult]]$Completions = @()

        if($Script:ModuleConfig.VerboseJson_ArgCompletions) {
            $Script:Cache
                | ConvertTo-Json -wa 'ignore' -depth 3
                | set-content -path (Join-Path (gi 'temp:\') 'CacheMeIfYouCan.ArgCompletions.log')
        }

        $script:Cache.GetEnumerator() | %{
            $Item = $_.Value
            $toCompleteAs = $Item.Name

            $Completions.Add(
                [CompletionResult]::new(
                    $toCompleteAs,
                    $toCompleteAs,
                    'ParameterValue',
                    $Item.Tooltip()
                ) )
            }

        if( $script:ModuleConfig.PrintExtraSummaryOnTabCompletion) {
            "`n" | write-host
            $Completions
                | format-table | out-string
                | write-host  #-bg $Script:Color.DimPurple
            "`n" | write-host
        }
        return $Completions
    }
}

'CachedKeyNameArgumentCompleter : not quite working, wip' | Write-host -bg 'darkorange'

function Cache._Write {
    <#
    .SYNOPSIS
        Internal helper, that encapsulates writing keys
 
 
    #>

    # [Alias('Cache._Write')]
    [CmdletBinding()]
    param(
    #
        [ValidateNotNullOrWhiteSpace()]
        [ArgumentCompleter( { [CachedKeyNameArgumentCompleter] } )]
        # [ValidateNotNullOrEmpty()]
        [Parameter(Mandatory, Position=0)]
        [string]$KeyName,

        # [ValidateNotNullOrWhiteSpace()]
        # [ValidateNotNullOrEmpty()]
        # [Parameter(Mandatory, ValueFromPipeline )]
        # maybe this should allow caching in circumstances
        [Parameter(Mandatory, Position = 1 )]
        [CachedRecord]$CachedRecord
    )
    if(-not $PSBoundParameters.ContainsKey('CachedRecord')){ throw 'MissingArgException' }
    if( $CachedRecord -isnot [CachedRecord] ) { throw 'ShouldNeverReachInvalidParameterTypeException' }

    $script:Cache[ $KeyName ] = $Record
    'wrote: {0}, calcDuration: {1}' -f @(
        $KeyName
        $CachedRecord.CalculationDuration
    )   | New-Text -fg $script:Colors.DimOrange
        | write-information -infa 'continue'
}


class CachedRecord { # Todo: make me
    [string]$Name
    # created by
    [scriptblock]$ScriptBlock
    [object]$Value
    [datetime]$EvaluatedAt
    [timespan]$CalculationDuration
    [hashtable]$Metadata

    [timespan]Age () {
        return [datetime]::Now - $This.EvaluatedAt
    }
    [string] ToString() {
        return '[CacheRecord := Name: {0}, Age: {1} ]' -f @(
            $This.Name
            $This.Age().TotalSeconds | Join-String -f '{0:n2}'
        )
    }
    [string] Tooltip() {
        # used for custom rendering for tooltips
        return $This.ToString()
    }
}
function CacheMe.ColorTest {
    $script:Color.GetEnumerator() | %{
        $_.Key | New-Text -fg $_.Value | Join-String
    }| Join-String -sep "`n"
}
function CacheMe.Clear {
    [Alias('Clear-CacheMe')]
    param()
    'clearing all cached values...' | write-host -fg 'orange'
    $script:Cache.Clear()
}
function Get-CacheMeRecords {
    <#
    .SYNOPSIS
        enumerates the raw [CachedRecord[]] records
    #>

    [OutputType( [CachedRecord] )]
    [Alias()]
    param()
    # ( $script:Cache.Keys ).count
    $script:Cache.GetEnumerator() | %{
        $_.Value
    }

}
function CacheMe.ListKeys {
    <#
    .SYNOPSIS
        Returns key names as untouched strings. Writes optional colors to to Write-Information
    .DESCRIPTION
        You'll
    .EXAMPLE
    Pwsh7 🐒
    > $names = CacheMe.ListKeys
    > $Names -join ', '
 
            All_Modules, gcm_less
 
    .EXAMPLE
    Pwsh7 🐒
        > $names = CacheMe.ListKeys -infa Continue | Join.UL
 
        keys => All_Modules, gcm_less
 
            - All_Modules
            - gcm_less
 
    .EXAMPLE
    Pwsh7 🐒
        > CacheMe.ListKeys | Join-String -sep ', ' -SingleQuote
            'All_Modules', 'gcm_less'
 
    .EXAMPLE
    Pwsh7 🐒>
        foreach($x in (CacheMe.ListKeys)) {
            "DoStuff: $x"
        }
    #>


    [Alias(
        'Get-CacheMe.Keys',
        'Get-CacheMeKeys'
    )]
    [CmdletBinding()]
    [OutputType('String[]')]
    param()

    $PSStyle.OutputRendering = 'ansi'
    Cache._Read -KeyName foo|ft -AutoSize | Out-String | Write-Information

    [string[]]$Keys = @($script:Cache).Keys?.Clone()
        | Sort-Object -Unique

    $Keys
        | join-String -sep ', ' -p {
            $_ | New-Text -fg 'gray70' -bg 'gray40'
        }   | Join-String -op 'keys => '
            | Write-Information

    return $Keys
}

function CacheMe.Add-CachedValue {
    <#
    .SYNOPSIS
        # currently 'Value' in the alias is redundant
        # whether it's Get or Set is based on whether -value Argument is included
    #>

    [Alias(
        # currently 'Value' in the alias is redundant
        # whether it's Get or Set is based on whether -value Argument is included
        # 'Add-CacheMeValue',
        'CacheMe.Value',
        # 'Add-CacheMeValue',
        'Add-CacheMe',
        # 'Get-CacheMeValue',
        'Get-CacheMe',
        # 'Set-CacheMeValue',
        'Set-CacheMe'

        # 'Set-CacheMeValue',
        # 'Set-CacheMeValue',
        # 'Cache.Value',
        # 'Cached'
    )]
    <#
    .SYNOPSIS
    minimal, fast, caching of random results
        - todo
        refresh: can I re-calc using scriptblock that's s aved?
    .EXAMPLE
        Cache.Value -Name 'gmo-noargs' -ScriptBlock { get-module } -Verbose -Force | % Value| Out-Null
    .EXAMPLE
        Cache.Value -Name <tab>'gmo-noargs'
            # returns value
    .EXAMPLE
        $x = Namo.CachedValue -Name 'gmo-list' { get-module -ListAvailable } -Verbose
        # totalTime: 0.02 ms
    #>

    [CmdletBinding()]
    param(
        [Alias('Label')]
        # [ArgumentCompleter( { [CachedKeyNameArgumentCompleter()] })]
        [ArgumentCompleter(  [CachedKeyNameArgumentCompleter] )]
        [string]$Name,

        [ValidateScript({throw 'nyi'})]
        # -PassThru returns the raw [CacheRecord], instead of the value
        [switch]$PassThru,

        [Alias('Expression', 'SB', 'E')]
        [scriptblock]$ScriptBlock,
        # force calculation
        [Alias('Rebuild')]
        [switch]$Force,

        # move to another cmdlet when exporting module
        [switch]$FullReset
    )
    if($FullReset) { $script:Cache.Clear()  }

    if( [string]::IsNullOrWhiteSpace($ScriptBlock)) {
        return (Cache._Read -KeyName $Name -Verbose )
    }


    [datetime]$Start = [datetime]::Now
    [datetime]$End = $Start
    if($Force -or ($script:Cache.count -eq 0) -or (-not $script:Cache.ContainsKey($Name))){
        try {
            # if a parameter binding exception is thrown, this isn't caught here
            $newValue = & $ScriptBlock
            $end = [datetime]::Now

            $record = [CachedRecord]@{
                CalculationDuration = $End - $Start
                EvaluatedAt = $Start
                Name = $Name
                ScriptBlock = $ScriptBlock
                Value = $newValue
                Metadata = @{
                    SourceCommand = $PSCommandPath
                    SourcePSScriptRoot = $PSScriptRoot
                    MyCmdName = $MyInvocation.MyCommand.Name
                    MyModule = $MyInvocation.MyCommand.ModuleName
                    MyScriptLine = $MyInvocation.ScriptLineNumber
                    MyScriptName = $MyInvocation.ScriptName
                    PathString = $MyInvocation | Join-String -p {
                        $_.ScriptName, $_.ScriptLineNumber -join ':' }
                }

            }
            Cache._Write -KeyName $Record.Name -CachedRecord $record -verb
            # $script:Cache[ $Name ] = $Record # $newValue
        } catch {
            $PSCommandPath
            'Namo.CachedValue( {0} ) invoked by {1}' -f @(
                $Name
                $PSCommandPath
            ) | write-verbose
            $_ | write-warning
            return $Null
        }
    }
    $delta = $end - $start

    $delta | join-String -p TotalMilliseconds -f 'total {0:n} ms'
    $delta
        | Join-String -p TotalMilliseconds -f 'total {0:n2} ms'
        | New-Text -fg $script:Color.DimPurple
        | Write-Information -infa 'Continue'

    # return @( $script:Cache[ $Name ] )?.Value

    return (Cache._Read -KeyName $Name -Verbose )
}

Export-ModuleMember -Function @(
    '*-CacheMe*'
    'CacheMe.*'

    if( $ModuleConfig.ExportDebugFunctions ) {
        # 'WarnIf.NotType'
        'Cache*_*'
    }
    if ($ModuleConfig.ExportAggressiveNames) {
        'Cache.*'
        'Cached*'
    }
) -Alias @(
    '*-CacheMe*'
    'CacheMe.*'
    if ($ModuleConfig.ExportAggressiveNames) {
        'Cached*'
        'Cache.*'
    }
    if( $ModuleConfig.ExportDebugFunctions ) {
        # 'WarnIf.NotType'
        'Cache*_*'
    }
) -Variable @(
    # '*CacheMe*'
    # 'CacheMe.*'
    if ($ModuleConfig.ExportAggressiveNames) {
        # 'Cache.*'
        # 'Cached*'
    }
)

if($ModuleConfig.ExportDebugFunctions) {
    'CacheMeIfYouCan::ExportDebugFunctions is enabled' | Write-warning
}
if($ModuleConfig.ExportAggressiveNames) {
    $ModuleConfig.ExportAggressiveNames
        | Join-String -op 'CacheMeIfYouCan::ModuleConfig.ExportAggressiveNames: '
        | write-warning
}