Write-Colour.psm1

using namespace System.Text

<#
.SYNOPSIS
Display strings in the host using multiple colours.

.DESCRIPTION
Takes an array of strings to display, and an array of console colours to use as
the foreground colour for each string. See the notes for more information.

.PARAMETER Message
Strings to display in the host.

.PARAMETER DefaultColour
Default foreground colour to use when displaying strings.

.PARAMETER Colour
Foreground colours to use when displaying strings.

.PARAMETER BackgroundColour
Background colour to use when displaying strings.
The same background colour will be used for every string.

.PARAMETER LinesBefore
Number of lines to output before the strings.
Note that lines before are never written to the log file.

.PARAMETER LinesAfter
Number of lines to output after the strings.
Note that lines after are never written to the log file.

.PARAMETER SpacesBefore
Number of spaces to output before the first string.

.PARAMETER SpacesAfter
Number of spaces to output after the last string.

.PARAMETER TabsBefore
Number of tabs to output before the first string.

.PARAMETER LogFile
Path to log file to which strings are written.
Defaults to '$Env:WriteColourLogFile', (falls back to '$Env:WriteColorLogFile') to allow for global logging.

.PARAMETER TimestampTimezone
ID of the timezone to use when displaying timestamps.
Available timezones can be found by running 'Get-TimeZone -ListAvailable'.

.PARAMETER TimestampColour
Colour of the timestamp to output before the first string.

.PARAMETER TimestampFormat
Format of the timestamp to output before the first string.

.PARAMETER NoTimestamp
Whether to omit the timestamp before the first string.

.PARAMETER NoNewLine
Whether there should be no new line at after output of the last string.

.PARAMETER Split
Whether to split strings so each is on a new line.

.INPUTS
None. You cannot pipe strings to this function.

.OUTPUTS
None. This function only displays text in the host.

.NOTES
There is no requirement for the number of colours specified in -Colour to match
the number of messages specified in -Message. In fact, there is no requirement
to specify -Colour at all. The -DefaultColour is will be used for each message
without a corrisponding -Colour.

Note that in reality, the strings displayed in the examples would be coloured.

.EXAMPLE
Write-Colour -Message "This ", "is ", "a ", "message" -Colour Red, Blue, Green, Yellow
This is a message
#>

function Write-Colour {
    [alias("wrc", "Write-Color")]
    param(
        [Parameter(Mandatory = $True, Position = 0, HelpMessage = "Strings to display in the host.")]
        [Alias("m", "t")]
        [ValidateNotNullOrEmpty()]
        [String[]]$Message,

        [Parameter(Mandatory = $False)]
        [Alias("d", "DefaultColor", "DefaultForegroundColour", "DefaultForegroundColor")]
        [ConsoleColor]$DefaultColour = [ConsoleColor]::White,

        [Parameter(Mandatory = $False)]
        [Alias("c", "Color", "ForegroundColour", "ForegroundColor")]
        [ConsoleColor[]]$Colour = $DefaultColour,

        [Parameter(Mandatory = $False)]
        [Alias("b", "BackgroundColor")]
        [ConsoleColor[]]$BackgroundColour,

        [Parameter(Mandatory = $False)]
        [Alias("lb")]
        [ValidateRange(0, 5)]
        [Int]$LinesBefore = 0,

        [Parameter(Mandatory = $False)]
        [Alias("la")]
        [ValidateRange(0, 5)]
        [Int]$LinesAfter = 0,

        [Parameter(Mandatory = $False)]
        [Alias("sb")]
        [ValidateRange(0, 40)]
        [Int]$SpacesBefore = 0,

        [Parameter(Mandatory = $False)]
        [Alias("sa")]
        [ValidateRange(0, 40)]
        [Int]$SpacesAfter = 0,

        [Parameter(Mandatory = $False)]
        [Alias("tb")]
        [ValidateRange(0, 5)]
        [Int]$TabsBefore = 0,

        [Parameter(Mandatory = $False)]
        [Alias("l")]
        [ValidateNotNull()]
        [AllowEmptyString()]
        [String]$LogFile = (($null -ne $Env:WriteColourLogFile) ? $Env:WriteColourLogFile : $Env:WriteColorLogFile),

        [Parameter(Mandatory = $False, DontShow)]
        [Alias('le')]
        [Encoding]$LogFileEncoding = [Encoding]::UTF8,

        [Parameter(Mandatory = $False)]
        [Alias("tz")]
        [ValidateScript( {
                try {
                    Get-TimeZone -Id $_ -ErrorAction Stop -ErrorVariable timezonErr
                }
                catch {
                    throw [ValidationMetadataException]::new($timezonErr.ErrorRecord.Exception.Message)
                }
            })]
        [String]$TimestampTimezone = "UTC",

        [Parameter(Mandatory = $False)]
        [Alias("tc", "TimestampColor")]
        [ConsoleColor[]]$TimestampColour = [ConsoleColor]::DarkGray,

        [Parameter(Mandatory = $False)]
        [Alias("tf")]
        [ValidateNotNullOrEmpty()]
        [String]$TimestampFormat = "[yyyy-MM-dd HH:mm:sszzz] ",

        [Parameter(Mandatory = $False)]
        [Alias("nt")]
        [Switch]$NoTimestamp,

        [Parameter(Mandatory = $False)]
        [Alias("nn")]
        [Switch]$NoNewLine,

        [Parameter(Mandatory = $False)]
        [Alias("s")]
        [Switch]$Split
    )
    begin {
        ### If a log file has been specified, ensure that it exists.
        if ($LogFile) {
            if (-not(Test-Path -Path $LogFile)) {
                New-Item -Path $logFile -ItemType File -Force
            }
        }

        ### Construct the timestamp, and inject it in to the strings to display.
        $timestamp = (-not($NoTimestamp)) ? [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TimestampTimezone).ToString($TimestampFormat) : ""
        if (-not($NoTimestamp)) {
            $Message = , $timestamp + $Message
            $Colour = , $TimestampColour + $Colour
        }

        ### Output lines before output of $Message begins.
        for ($i = 0; $i -lt $LinesBefore; $i++) {
            $Message = , ("{0}`n" -f $timestamp) + $Message
            $Colour = , $TimestampColour + $Colour
        }

        ### Add spaces before the first string, and after the last string.
        if ($SpacesBefore -gt 0) {
            $Message[0] = "{0}{1}" -f (" " * $SpacesBefore), $Message[0]
        }
        if ($SpacesAfter -gt 0) {
            $lastMessageIndex = $Message.Count - 1
            $Message[$lastMessageIndex] = "{0}{1}" -f $Message[$lastMessageIndex], (" " * $SpacesAfter)
        }

        ### Add tabs before the first string.
        if ($TabsBefore -gt 0) {
            $Message[0] = "{0}{1}" -f ("`t" * $TabsBefore), $Message[0]
        }

        ### Create an array for the parameters that will be used when displaying strings in the host.
        $paramsArr = @()
    }
    process {
        ### Iterate the $Message string, with the purpose of this block being to minimise the number of 'Write-Host' commands that are executed.
        for ($i = 0; $i -lt $Message.Count; $i++) {
            $lastParamsIndex = $paramsArr.Count - 1
            $currentForegroundColour = ($null -ne $Colour[$i]) ? $Colour[$i] : $DefaultColour
            $currentBackgroundColour = ($null -ne $BackgroundColour) ? $BackgroundColour : ""
            $currentNoNewLine = (($Split -eq $True) -and (-not($i -eq $Message.Count - 1))) ? $False : $True

            ### Check whether the current string should use the same foreground color as the last message.
            ### -- If $True, then append the string to the last 'Object' in the $paramsArr.
            ### -- Else, create a new set of parameters in the $paramsArr.
            if ($currentForegroundColour -eq $paramsArr[$lastParamsIndex].ForegroundColor) {
                $paramsArr[$lastParamsIndex].Object += (($Split -eq $True) ? "`n" : "") + $Message[$i]
                $paramsArr[$lastParamsIndex].NoNewLine = $currentNoNewLine
            }
            else {
                $tempParams = @{
                    Object          = $Message[$i]
                    ForegroundColor = $currentForegroundColour
                    BackgroundColor = $currentBackgroundColour
                    NoNewLine       = $currentNoNewLine
                }
                ($tempParams.GetEnumerator() | Where-Object { -not $_.Value }) | ForEach-Object { $tempParams.Remove($_.Name) }
                $paramsArr += $tempParams
            }
        }

        ### Itterate $paramsArr array and display strings in the host.
        for ($i = 0; $i -lt $paramsArr.Count; $i++) {
            $params = $paramsArr[$i]
            Write-Host @params
            if (-not([String]::IsNullOrWhiteSpace($LogFile))) { $params.Object | Add-Content -Path $LogFile -Encoding $LogFileEncoding -NoNewline }
        }
        
        ### Output a new line at the end of the message, unless one should be specifically omitted.
        if (-not($NoNewLine)) {
            Write-Output ""
            if (-not([String]::IsNullOrWhiteSpace($LogFile))) { "" | Add-Content -Path $LogFile -Encoding $LogFileEncoding }
        }
    }
    end {
        ### Output lines after output of $Message ends.
        for ($i = 0; $i -lt $LinesAfter; $i++) {
            Write-Host $timestamp -ForegroundColor $TimestampColour
        }
    }
}