tiPS.psm1

#Requires -Version 3.0
Set-StrictMode -Version Latest

# All module functions that reference a file path in the module should use this variable, rather than PSScriptRoot.
New-Variable -Name 'PSModuleRoot' -Value $PSScriptRoot -Option Constant -Scope Script

if (-not ('tiPS.PowerShellTip' -as [type]))
{
    [string] $assemblyFilePath = Resolve-Path -Path "$script:PSModuleRoot/Classes/tiPSClasses.dll"
    Add-Type -Path $assemblyFilePath
}

function StartModuleUpdateIfNeeded
{
    [CmdletBinding()]
    [OutputType([void])]
    Param
    (
        [Parameter(Mandatory = $true, HelpMessage = 'The tiPS configuration to use to determine if the module should be updated.')]
        [ValidateNotNullOrEmpty()]
        [tiPS.Configuration] $Config
    )

    # For performance reasons, check if we should never update the module before doing anything else.
    if ($Config.AutoUpdateCadence -eq [tiPS.ModuleAutoUpdateCadence]::Never)
    {
        return
    }

    [DateTime] $modulesLastUpdateDate = ReadModulesLastUpdateDateOrDefault
    [TimeSpan] $timeSinceLastUpdate = [DateTime]::Now - $modulesLastUpdateDate
    [int] $daysSinceLastUpdate = $timeSinceLastUpdate.Days

    [bool] $moduleUpdateNeeded = $false
    switch ($Config.AutoUpdateCadence)
    {
        ([tiPS.ModuleAutoUpdateCadence]::Never) { $moduleUpdateNeeded = $false; break }
        ([tiPS.ModuleAutoUpdateCadence]::Daily) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 1; break }
        ([tiPS.ModuleAutoUpdateCadence]::Weekly) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 7 ; break }
        ([tiPS.ModuleAutoUpdateCadence]::BiWeekly) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 14; break }
        ([tiPS.ModuleAutoUpdateCadence]::Monthly) { $moduleUpdateNeeded = $daysSinceLastUpdate -ge 30; break }
    }

    if ($moduleUpdateNeeded)
    {
        UpdateModule
    }
    else
    {
        Write-Debug "An auto-update of the tiPS module is not needed at this time."
    }
}

function UpdateModule
{
    [CmdletBinding()]
    [OutputType([void])]
    Param()

    Write-Verbose "Updating the tiPS module in a background job."
    Start-Job -ScriptBlock {
        Write-Verbose "Updating the tiPS module."
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        Update-Module -Name 'tiPS' -Force

        Write-Verbose "Removing all but the latest version of the tiPS module to keep the modules directory clean."
        $latestModuleVersion = Get-InstalledModule -Name 'tiPS'
        $allModuleVersions = Get-InstalledModule -Name 'tiPS' -AllVersions
        $allModuleVersions |
            Where-Object { $_.Version -ne $latestModuleVersion.Version } |
            Uninstall-Module -Force
    }

    [DateTime] $todayWithoutTime = [DateTime]::Now.Date # Exclude the time for a better user experience.
    WriteModulesLastUpdateDate -ModulesLastUpdateDate $todayWithoutTime
}

function ReadModulesLastUpdateDateOrDefault
{
    [CmdletBinding()]
    [OutputType([DateTime])]
    Param()

    [DateTime] $modulesLastUpdateDate = [DateTime]::MinValue
    [string] $moduleUpdateDateFilePath = GetModulesLastUpdateDateFilePath
    if (Test-Path -Path $moduleUpdateDateFilePath -PathType Leaf)
    {
        [string] $modulesLastUpdateDateString = [System.IO.File]::ReadAllText($moduleUpdateDateFilePath)
        $modulesLastUpdateDate = [DateTime]::Parse($modulesLastUpdateDateString)
    }
    return $modulesLastUpdateDate
}

function WriteModulesLastUpdateDate
{
    [CmdletBinding()]
    [OutputType([void])]
    Param
    (
        [DateTime] $ModulesLastUpdateDate
    )

    [string] $moduleUpdateDateFilePath = GetModulesLastUpdateDateFilePath
    Write-Verbose "Writing modules last update date '$ModulesLastUpdateDate' to '$moduleUpdateDateFilePath'."
    $ModulesLastUpdateDate.ToString() | Set-Content -Path $moduleUpdateDateFilePath -Force -NoNewline
}

function GetModulesLastUpdateDateFilePath
{
    [CmdletBinding()]
    [OutputType([string])]
    Param()

    [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath
    [string] $moduleUpdateDateFilePath = Join-Path -Path $appDataDirectoryPath -ChildPath 'ModulesLastUpdateDate.txt'
    return $moduleUpdateDateFilePath
}

function WriteAutomaticPowerShellTipIfNeeded
{
    [CmdletBinding()]
    [OutputType([void])]
    Param
    (
        [Parameter(Mandatory = $true, HelpMessage = 'The tiPS configuration used to determine if a tip should be written.')]
        [ValidateNotNullOrEmpty()]
        [tiPS.Configuration] $Config
    )

    # For performance reasons, check if we should never write a tip before doing anything else.
    if ($Config.AutoWritePowerShellTipCadence -eq [tiPS.WritePowerShellTipCadence]::Never)
    {
        return
    }

    [DateTime] $lastAutomaticTipWrittenDate = ReadLastAutomaticTipWrittenDateOrDefault
    [TimeSpan] $timeSinceLastAutomaticTipWritten = [DateTime]::Now - $lastAutomaticTipWrittenDate
    [int] $daysSinceLastAutomaticTipWritten = $timeSinceLastAutomaticTipWritten.Days

    [bool] $shouldShowTip = $false
    switch ($Config.AutoWritePowerShellTipCadence)
    {
        ([tiPS.WritePowerShellTipCadence]::Never) { $shouldShowTip = $false; break }
        ([tiPS.WritePowerShellTipCadence]::EverySession) { $shouldShowTip = $true; break }
        ([tiPS.WritePowerShellTipCadence]::Daily) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 1; break }
        ([tiPS.WritePowerShellTipCadence]::Weekly) { $shouldShowTip = $daysSinceLastAutomaticTipWritten -ge 7; break }
    }

    if ($shouldShowTip)
    {
        [bool] $isSessionInteractive = TestPowerShellSessionIsInteractive
        if (-not $isSessionInteractive)
        {
            Write-Verbose "tiPS is configured to write an automatic tip now, but this session is non-interactive. tiPS will only write automatic tips when it is imported into an interactive PowerShell session. This prevents a tip from being written at unexpected times, such as when the user or an automated process runs PowerShell scripts."
            return
        }

        WriteAutomaticPowerShellTip
    }
    else
    {
        Write-Debug "Showing a tiPS PowerShell tip is not needed at this time."
    }
}

function WriteAutomaticPowerShellTip
{
    [CmdletBinding()]
    [OutputType([void])]
    Param()

    Write-PowerShellTip

    [DateTime] $todayWithoutTime = [DateTime]::Now.Date # Exclude the time for a better user experience.
    WriteLastAutomaticTipWrittenDate -LastAutomaticTipWrittenDate $todayWithoutTime
}

function TestPowerShellSessionIsInteractive
{
    [CmdletBinding()]
    [OutputType([bool])]
    Param()

    if (-not [Environment]::UserInteractive)
    {
        Write-Debug "The [Environment]::UserInteractive property shows this PowerShell session is not interactive."
        return $false
    }

    [string[]] $typicalNonInteractiveCommandLineArguments = @(
        '-Command'
        '-c'
        '-EncodedCommand'
        '-e'
        '-ec'
        '-File'
        '-f'
        '-NonInteractive'
    )

    [string[]] $commandLineArgs = [Environment]::GetCommandLineArgs()
    Write-Debug "The PowerShell command line arguments are '$commandLineArgs'."

    [string[]] $nonInteractiveArgMatches = $commandLineArgs |
        Where-Object { $_ -in $typicalNonInteractiveCommandLineArguments }
    [bool] $isNonInteractive = $null -ne $nonInteractiveArgMatches -and $nonInteractiveArgMatches.Count -gt 0

    if ($isNonInteractive)
    {
        return $false
    }

    return $true
}

function ReadLastAutomaticTipWrittenDateOrDefault
{
    [CmdletBinding()]
    [OutputType([DateTime])]
    Param()

    [DateTime] $lastAutomaticTipWrittenDate = [DateTime]::MinValue
    [string] $lastAutomaticTipWrittenDateFilePath = GetLastAutomaticTipWrittenDateFilePath
    if (Test-Path -Path $lastAutomaticTipWrittenDateFilePath -PathType Leaf)
    {
        [string] $lastAutomaticTipWrittenDateString = [System.IO.File]::ReadAllText($lastAutomaticTipWrittenDateFilePath)
        $lastAutomaticTipWrittenDate = [DateTime]::Parse($lastAutomaticTipWrittenDateString)
    }
    return $lastAutomaticTipWrittenDate
}

function WriteLastAutomaticTipWrittenDate
{
    [CmdletBinding()]
    [OutputType([void])]
    Param
    (
        [DateTime] $LastAutomaticTipWrittenDate
    )

    [string] $lastAutomaticTipWrittenDateFilePath = GetLastAutomaticTipWrittenDateFilePath
    Write-Verbose "Writing last automatic tip Written date '$LastAutomaticTipWrittenDate' to '$lastAutomaticTipWrittenDateFilePath'."
    $LastAutomaticTipWrittenDate.ToString() | Set-Content -Path $lastAutomaticTipWrittenDateFilePath -Force -NoNewline
}

function GetLastAutomaticTipWrittenDateFilePath
{
    [CmdletBinding()]
    [OutputType([string])]
    Param()

    [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath
    [string] $lastAutomaticTipWrittenDateFilePath = Join-Path -Path $appDataDirectoryPath -ChildPath 'LastAutomaticTipWrittenDate.txt'
    return $lastAutomaticTipWrittenDateFilePath
}

function GetConfigurationFilePath
{
    [CmdletBinding()]
    [OutputType([string])]
    Param()

    [string] $appDataDirectoryPath = Get-TiPSDataDirectoryPath
    [string] $configFilePath = Join-Path -Path $appDataDirectoryPath -ChildPath 'tiPSConfiguration.json'
    return $configFilePath
}

function ReadConfigurationFromFileOrDefault
{
    [CmdletBinding()]
    [OutputType([tiPS.Configuration])]
    Param()

    $config = [tiPS.Configuration]::new()
    [string] $configFilePath = GetConfigurationFilePath
    if (Test-Path -Path $configFilePath -PathType Leaf)
    {
        Write-Verbose "Reading configuration from '$configFilePath'."
        $config = [System.IO.File]::ReadAllText($configFilePath) | ConvertFrom-Json
    }
    return $config
}

function WriteConfigurationToFile
{
    [CmdletBinding()]
    [OutputType([void])]
    Param
    (
        [tiPS.Configuration] $Config
    )

    [string] $configFilePath = GetConfigurationFilePath

    if (-not (Test-Path -Path $configFilePath -PathType Leaf))
    {
        New-Item -Path $configFilePath -ItemType File -Force > $null
    }

    Write-Verbose "Writing configuration to '$configFilePath'."
    $Config | ConvertTo-Json -Depth 100 | Set-Content -Path $configFilePath -Force
}

function InitializeModule
{
    [CmdletBinding()]
    [OutputType([void])]
    Param()

    Write-Debug 'Reading all tips from JSON file and storing them in a $Tips variable for access by other module functions.'
    [hashtable] $tipHashTable = ReadAllPowerShellTipsFromJsonFile
    New-Variable -Name 'Tips' -Value $tipHashtable -Option Constant -Scope Script

    Write-Debug 'Reading in configuration from JSON file and storing it in a $TiPSConfiguration variable for access by other module functions.'
    [tiPS.Configuration] $config = ReadConfigurationFromFileOrDefault
    New-Variable -Name 'TiPSConfiguration' -Value $config -Scope Script

    Write-Debug "Checking if we should write a PowerShell tip, and writing one if needed."
    WriteAutomaticPowerShellTipIfNeeded -Config $script:TiPSConfiguration

    Write-Debug 'Checking if the module needs to be updated, and updating it if needed.'
    StartModuleUpdateIfNeeded -Config $script:TiPSConfiguration
}

function ReadAllPowerShellTipsFromJsonFile
{
    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    Param()

    [string] $powerShellTipsJsonFilePath = Join-Path -Path $script:PSModuleRoot -ChildPath 'PowerShellTips.json'

    Write-Verbose "Reading PowerShell tips from '$powerShellTipsJsonFilePath'."
    [tiPS.PowerShellTip[]] $tipObjects =
        [System.IO.File]::ReadAllText($powerShellTipsJsonFilePath) | # Use .NET method instead of Get-Content for performance.
        ConvertFrom-Json

    [hashtable] $tipHashtable = [ordered]@{}
    foreach ($tip in $tipObjects)
    {
        $tipHashtable[$tip.Id] = $tip
    }

    return $tipHashtable
}

function WritePowerShellTipToTerminal
{
    [CmdletBinding()]
    [OutputType([void])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
    Param
    (
        [Parameter(Mandatory = $true, HelpMessage = 'The PowerShell Tip to write to the terminal.')]
        [ValidateNotNullOrEmpty()]
        [tiPS.PowerShellTip] $Tip
    )

    [ConsoleColor] $headerColor = [ConsoleColor]::Cyan
    [ConsoleColor] $tipTextColor = [ConsoleColor]::White
    [ConsoleColor] $exampleColor = [ConsoleColor]::Yellow
    [ConsoleColor] $urlsColor = [ConsoleColor]::Green
    [ConsoleColor] $minPowerShellVersionColor = [ConsoleColor]::Red

    # Calculate how many header characters to put on each side of the title to make it look nice.
    [int] $numberOfCharactersInHeader = 90
    [int] $headerContentLength = $Tip.Title.Length + 2 + $Tip.Category.ToString().Length + 3
    [int] $numberOfHeaderCharactersOnEachSideOfTitle =
        [Math]::Floor(($numberOfCharactersInHeader - ($headerContentLength)) / 2)
    [int] $additionalHeaderCharacterNeeded = 0
    if ($headerContentLength % 2 -eq 1)
    {
        $additionalHeaderCharacterNeeded = 1
    }

    [string] $header =
        ('-' * $numberOfHeaderCharactersOnEachSideOfTitle) +
        ' ' + $Tip.Title + ' ' +
        '[' + $Tip.Category + '] ' +
        ('-' * ($numberOfHeaderCharactersOnEachSideOfTitle + $additionalHeaderCharacterNeeded))
    Write-Host $header -ForegroundColor $headerColor

    Write-Host $Tip.TipText -ForegroundColor $tipTextColor

    if (-not [string]::IsNullOrWhiteSpace($Tip.Example))
    {
        Write-Host 'Example: ' -ForegroundColor $exampleColor -NoNewline
        Write-Host $Tip.Example -ForegroundColor $exampleColor
    }

    if ($Tip.UrlsAreProvided)
    {
        Write-Host 'More information: ' -ForegroundColor $urlsColor -NoNewline
        Write-Host $Tip.Urls -ForegroundColor $urlsColor
    }

    if ($Tip.MinPowerShellVersionIsProvided)
    {
        Write-Host 'Required PowerShell version or greater: ' -ForegroundColor $minPowerShellVersionColor -NoNewline
        Write-Host $Tip.MinPowerShellVersion -ForegroundColor $minPowerShellVersionColor
    }

    Write-Host ('-' * $numberOfCharactersInHeader) -ForegroundColor $headerColor
}

function Edit-PowerShellProfileToImportTiPS
{
<#
    .SYNOPSIS
    Edits the user's PowerShell profile file to import the tiPS module.
 
    .DESCRIPTION
    This function edits the user's PowerShell profile file to import the tiPS module, which can provide
    automatic tips and updates. If the profile already imports the tiPS module, then no changes are made.
    Only the default PowerShell profile paths are searched to see if the tiPS module is already imported; if
    it is imported from a dot-sourced script, the function will not detect that and will add an import statement
    directly to the profile file.
 
    .INPUTS
    None. You cannot pipe objects to the function.
 
    .OUTPUTS
    None. The function does not return any objects.
 
    .EXAMPLE
    Edit-PowerShellProfileToImportTiPS
 
    This example edits the PowerShell profile to import the tiPS module.
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([void])]
    Param()

    Process
    {
        [bool] $moduleImportStatementIsAlreadyInProfile = Test-PowerShellProfileImportsTiPS
        if ($moduleImportStatementIsAlreadyInProfile)
        {
            Write-Verbose "PowerShell profile already imports the tiPS module, so no changes are necessary."
            return
        }

        [string] $profileFilePath = GetPowerShellProfileFilePath
        [string] $contentToAddToProfile = 'Import-Module -Name tiPS # Added by tiPS to get automatic tips and updates.'

        if ([string]::IsNullOrWhiteSpace($profileFilePath))
        {
            Write-Error "Could not determine the PowerShell profile file path."
            return
        }

        if (-not (Test-Path -Path $profileFilePath -PathType Leaf))
        {
            if ($PSCmdlet.ShouldProcess("PowerShell profile '$profileFilePath'", 'Create'))
            {
                Write-Verbose "Creating PowerShell profile '$profileFilePath'."
                New-Item -Path $profileFilePath -ItemType File -Force > $null
            }
        }

        if ($PSCmdlet.ShouldProcess("PowerShell profile '$profileFilePath'", 'Update'))
        {
            Write-Verbose "Adding '$contentToAddToProfile' to PowerShell profile '$profileFilePath'."
            Add-Content -Path $profileFilePath -Value $contentToAddToProfile -Force
        }
    }
}

# Use a function to get the file path so we can mock this function for testing.
function GetPowerShellProfileFilePath
{
    [string] $profileFilePath = [string]::Empty

    # The $PROFILE variable may not exist depending on the host or the context in which PowerShell was started.
    if (Test-Path -Path variable:PROFILE)
    {
        $profileFilePath = $PROFILE.CurrentUserAllHosts
    }

    return $profileFilePath
}

function Get-PowerShellTip
{
<#
    .SYNOPSIS
    Get a PowerShellTip object. If no parameters are specified, a random tip is returned.
 
    .DESCRIPTION
    Get a PowerShellTip object. If no parameters are specified, a random tip is returned.
 
    .PARAMETER Id
    The ID of the tip to retrieve. If not supplied, a random tip will be returned.
 
    .PARAMETER AllTips
    Return all tips.
 
    .INPUTS
    You can pipe a [string] of the ID of the tip to retrieve, or a PsCustomObject with a [string] 'Id' property.
 
    .OUTPUTS
    A [tiPS.PowerShellTip] object representing the PowerShell tip.
 
    If the -AllTips switch is provided, a [System.Collections.Specialized.OrderedDictionary] is returned.
 
    .EXAMPLE
    Get-PowerShellTip
 
    Get a random tip.
 
    .EXAMPLE
    Get-PowerShellTip -Id '2023-07-16-powershell-is-open-source'
 
    Get the tip with the specified ID. If no tip with the specified ID exists, an error is written.
 
    .EXAMPLE
    Get-PowerShellTip -AllTips
 
    Get all tips.
 
    .EXAMPLE
    '2023-07-16-powershell-is-open-source' | Get-PowerShellTip
 
    Pipe a [string] of the ID of the tip to retrieve.
 
    .EXAMPLE
    [PSCustomObject]@{ Id = '2023-07-16-powershell-is-open-source' } | Get-PowerShellTip
 
    Pipe an object with a [string] 'Id' property.
#>


    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([tiPS.PowerShellTip], ParameterSetName = 'Default')]
    [OutputType([System.Collections.Specialized.OrderedDictionary], ParameterSetName = 'AllTips')]
    Param
    (
        [Parameter(ParameterSetName = 'Default', Mandatory = $false,
            ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'The ID of the tip to retrieve. If not supplied, a random tip will be returned.')]
        [string] $Id,

        [Parameter(ParameterSetName = 'AllTips', Mandatory = $false, HelpMessage = 'Return all tips.')]
        [switch] $AllTips
    )

    Process
    {
        if ($AllTips)
        {
            return ReadAllPowerShellTipsFromJsonFile
        }

        if ([string]::IsNullOrWhiteSpace($Id))
        {
            $Id = $script:Tips.Keys | Get-Random -Count 1
        }
        else
        {
            if (-not $script:Tips.ContainsKey($Id))
            {
                Write-Error "A tip with ID '$Id' does not exist."
                return
            }
        }

        [tiPS.PowerShellTip] $tip = $script:Tips[$Id]
        return $tip
    }
}

function Get-TiPSConfiguration
{
<#
    .SYNOPSIS
    Get the tiPS module configuration for the current user.
 
    .DESCRIPTION
    Get the tiPS module configuration for the current user.
 
    .INPUTS
    None. You cannot pipe objects to the function.
 
    .OUTPUTS
    A [tiPS.Configuration] object containing all of the tiPS module configuration for the current user.
 
    .EXAMPLE
    Get-TiPSConfiguration
 
    Get the tiPS module configuration.
#>


    [CmdletBinding()]
    [OutputType([tiPS.Configuration])]
    Param()

    return $script:TiPSConfiguration
}

function Get-TiPSDataDirectoryPath
{
<#
    .SYNOPSIS
    Get the tiPS data directory path.
 
    .DESCRIPTION
    Get the tiPS data directory path where the tiPS module stores all of its data for the current user.
 
    .INPUTS
    None. You cannot pipe objects to the function.
 
    .OUTPUTS
    A [string] of the directory path.
 
    .EXAMPLE
    Get-TiPSDataDirectoryPath
 
    Get the tiPS data directory path.
#>

    [CmdletBinding()]
    [OutputType([string])]
    Param()

    [string] $usersLocalAppDataPath =
        [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)
    [string] $appDataDirectoryPath =
        Join-Path -Path $usersLocalAppDataPath -ChildPath (
        Join-Path -Path 'PowerShell' -ChildPath 'tiPS')
    return $appDataDirectoryPath
}

function Set-TiPSConfiguration
{
<#
    .SYNOPSIS
    Set the tiPS configuration.
 
    .DESCRIPTION
    Set the entire or partial tiPS configuration.
 
    .PARAMETER Configuration
    The tiPS configuration object to set.
    All configuration properties are updated to match the provided object.
    No other properties may be provided when this parameter is used.
 
    .PARAMETER AutomaticallyUpdateModule
    Whether to automatically update the tiPS module at session startup.
    The module update is performed in a background job, so it does not block the PowerShell session from starting.
    This also means that the new module version will not be used until the next time the module is imported, or
    the next time a PowerShell session is started.
    Old versions of the module are automatically deleted after a successful update.
    Valid values are Never, Daily, Weekly, Monthly, and Yearly. Default is Never.
 
    .PARAMETER AutomaticallyWritePowerShellTip
    Whether to automatically write a PowerShell tip at session startup.
    Valid values are Never, Daily, Weekly, Monthly, and Yearly. Default is Never.
 
    .INPUTS
    You can pipe a [tiPS.Configuration] object containing the tiPS configuration to set, or
    a PsCustomObject with AutomaticallyUpdateModule and/or AutomaticallyWritePowerShellTip properties.
 
    .OUTPUTS
    None. The function does not return any objects.
 
    .EXAMPLE
    Set-TiPSConfiguration -Configuration $config
 
    Set the tiPS configuration.
 
    .EXAMPLE
    Set-TiPSConfiguration -AutomaticallyUpdateModule Weekly
 
    Set the tiPS configuration to automatically update the tiPS module every 7 days.
 
    .EXAMPLE
    Set-TiPSConfiguration -AutomaticallyWritePowerShellTip Daily
 
    Set the tiPS configuration to automatically write a PowerShell tip every day.
 
    .EXAMPLE
    Set-TiPSConfiguration -AutomaticallyUpdateModule Never -AutomaticallyWritePowerShellTip Never
 
    Set the tiPS configuration to never automatically update the tiPS module or write a PowerShell tip.
#>

    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'PartialConfiguration')]
    [OutputType([void])]
    Param
    (
        [Parameter(Mandatory = $true, ParameterSetName = 'EntireConfiguration', ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [tiPS.Configuration] $Configuration,

        [Parameter(Mandatory = $false, ParameterSetName = 'PartialConfiguration', ValueFromPipelineByPropertyName = $true)]
        [tiPS.ModuleAutoUpdateCadence] $AutomaticallyUpdateModule = [tiPS.ModuleAutoUpdateCadence]::Never,

        [Parameter(Mandatory = $false, ParameterSetName = 'PartialConfiguration', ValueFromPipelineByPropertyName = $true)]
        [tiPS.WritePowerShellTipCadence] $AutomaticallyWritePowerShellTip = [tiPS.WritePowerShellTipCadence]::Never
    )

    Process
    {
        # If the entire Configuration object parameter is passed in, set it and return.
        if ($PSBoundParameters.ContainsKey('Configuration'))
        {
            if ($PSCmdlet.ShouldProcess('tiPS configuration', 'Set'))
            {
                $script:TiPSConfiguration = $Configuration
                WriteConfigurationToFile -Config $script:TiPSConfiguration
            }
        }

        # If the AutomaticallyUpdateModule parameter is passed in, set it.
        if ($PSBoundParameters.ContainsKey('AutomaticallyUpdateModule'))
        {
            if ($PSCmdlet.ShouldProcess('tiPS configuration AutoUpdateCadence property', 'Set'))
            {
                $script:TiPSConfiguration.AutoUpdateCadence = $AutomaticallyUpdateModule
                WriteConfigurationToFile -Config $script:TiPSConfiguration
            }
        }

        # If the AutomaticallyWritePowerShellTip parameter is passed in, set it.
        if ($PSBoundParameters.ContainsKey('AutomaticallyWritePowerShellTip'))
        {
            if ($PSCmdlet.ShouldProcess('tiPS configuration AutoWritePowerShellTipCadence property', 'Set'))
            {
                $script:TiPSConfiguration.AutoWritePowerShellTipCadence = $AutomaticallyWritePowerShellTip
                WriteConfigurationToFile -Config $script:TiPSConfiguration
            }
        }

        [bool] $automaticActionsAreConfigured =
            $script:TiPSConfiguration.AutoUpdateCadence -ne [tiPS.ModuleAutoUpdateCadence]::Never -or
            $script:TiPSConfiguration.AutoWritePowerShellTipCadence -ne [tiPS.WritePowerShellTipCadence]::Never
        if ($automaticActionsAreConfigured)
        {
            [bool] $tiPSModuleIsImportedByPowerShellProfile = Test-PowerShellProfileImportsTiPS
            if (-not $tiPSModuleIsImportedByPowerShellProfile)
            {
                Write-Warning "tiPS can only perform automatic actions when it is imported into the current PowerShell session. Run 'Edit-ProfileToImportTiPS' to update your PowerShell profile import tiPS automatically when a new session starts, or manually add 'Import-Module -Name tiPS' to your profile file. If you are importing the module in a different way, such as in a script that is dot-sourced into your profile, you can ignore this warning."
            }
        }
    }
}

function Test-PowerShellProfileImportsTiPS
{
<#
    .SYNOPSIS
    Tests whether the PowerShell profile imports the tiPS module.
 
    .DESCRIPTION
    Tests whether the PowerShell profile imports the tiPS module.
    Returns true if it finds an 'Import-Module -Name tiPS' statement in the profile, false otherwise.
    This only looks in the default PowerShell profile paths.
    If the tiPS module is imported from a dot-sourced file then this will return false.
 
    .INPUTS
    None. You cannot pipe objects to the function.
 
    .OUTPUTS
    System.Boolean representing if the tiPS module is imported by the PowerShell profile or not.
 
    .EXAMPLE
    Test-PowerShellProfileImportsTiPS
 
    Tests whether the PowerShell profile imports the tiPS module, returning true if it does and false otherwise.
 
    .EXAMPLE
    Test-PowerShellProfileImportsTiPS -Verbose
 
    Tests whether the PowerShell profile imports the tiPS module, returning true if it does and false otherwise.
    If true, the verbose output will list the profile file paths and the lines that import the tiPS module.
    If false, the verbose output will list the profile file paths that it checked.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    Param()

    [string[]] $powerShellProfileFilePaths = GetPowerShellProfileFilePaths
    [string[]] $profileFilePathsThatExist =
        $powerShellProfileFilePaths |
        Where-Object { Test-Path -Path $_ -PathType Leaf }

    if ($null -eq $profileFilePathsThatExist -or $profileFilePathsThatExist.Count -eq 0)
    {
        Write-Verbose "No PowerShell profile files exist."
        return $false
    }

    [string] $requiredContentRegex = 'Import-Module\s.*tiPS'
    [Microsoft.PowerShell.Commands.MatchInfo] $results =
        Select-String -Path $profileFilePathsThatExist -Pattern $requiredContentRegex
    if ($null -ne $results)
    {
        Write-Verbose "The tiPS module is imported by the following profile lines:"
        $results | ForEach-Object {
            Write-Verbose " $($_.Path): $($_.Line)"
        }
        return $true
    }

    Write-Verbose "The tiPS module is not imported by any of the PowerShell profiles: $profileFilePathsThatExist"
    return $false
}

# Use a function to get the file paths so we can mock this function for testing.
function GetPowerShellProfileFilePaths
{
    [string[]] $profileFilePaths = @()

    # The $PROFILE variable may not exist depending on the host or the context in which PowerShell was started.
    if (Test-Path -Path variable:PROFILE)
    {
        $profileFilePaths = @(
            $PROFILE.CurrentUserAllHosts
            $PROFILE.CurrentUserCurrentHost
            $PROFILE.AllUsersAllHosts
            $PROFILE.AllUsersCurrentHost
        )
    }

    return ,$profileFilePaths
}

function Write-PowerShellTip
{
<#
    .SYNOPSIS
    Write a PowerShell tip to the terminal.
 
    .DESCRIPTION
    Write a PowerShell tip to the terminal. If no tip is specified, a random tip will be written.
    The tip is written to the terminal using the Write-Host cmdlet so that colours can be applied.
    Thus, the tip is not written to the pipeline and cannot be captured in a variable.
    If you want to capture the tip in a variable, use the Get-PowerShellTip function.
 
    .PARAMETER Id
    The ID of the tip to write. If not supplied, a random tip will be written.
    If no tip with the specified ID exists, an error is written.
 
    .INPUTS
    You can pipe a [string] of the ID of the tip to write, or a PsCustomObject with a [string] 'Id' property.
 
    .OUTPUTS
    None. The function does not return any objects.
 
    .EXAMPLE
    Write-PowerShellTip
 
    Write a random tip to the terminal.
 
    .EXAMPLE
    Write-PowerShellTip -Id '2023-07-16-powershell-is-open-source'
 
    Write the tip with the specified ID.
#>

    [CmdletBinding()]
    [Alias('Tips')]
    [OutputType([void])]
    Param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true,
            HelpMessage = 'The ID of the tip to write. If not supplied, a random tip will be written.')]
        [string] $Id
    )

    Process
    {
        [tiPS.PowerShellTip] $tip = Get-PowerShellTip -Id $Id
        WritePowerShellTipToTerminal -Tip $tip
    }
}



Write-Debug 'Now that all types and functions are imported, initializing the module.'
InitializeModule

# Function and Alias exports are defined in the modules manifest (.psd1) file.