Gumby.Log.psm1

# abstain from using module Assert here as it might use the Log module

enum LogMessageType {
    Comment
    Warning
    Error
    Success
    Failure
    BeginSection
    EndSection
}

# Design considerations:
# I want to enable the use of Log calls without having to worry about performance. Therefore, Log
# calls in the absence of log listeners should have a negligible overhead.
#
# Should taking of time stamps be centralized in the log dispatcher?
# I think not for not all log outputs might utilize them (e.g. a file-based listener might while a
# screen-based listener might not).
#
# Should verbosity-gating be centralized in the log dispatcher?
# I think not. It is conceivable to have a run with multiple listeners that operate with different
# verbosity settings (e.g. a screen-based listener that shows only "important" output while a
# file-based listener logs everything).
#
# Should the log dispatcher keep track of nesting depth (sections)?
# I think not for not all log outputs might utilize them (e.g. an XML-based log listener might
# represent sections with respective tags rather than through indentation).

# Would 'Trace' be a better name?
class Log {
    static [void] Comment([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::Comment, $Message)
    }

    static [void] Trace([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::Comment, $Message)
    }

    static [void] Warning([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::Warning, $Message)
        ++[Log]::_warningCount
    }

    static [void] Error([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::Error, $Message)
        ++[Log]::_errorCount
    }

    static [void] Success([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::Success, $Message)
        ++[Log]::_successCount
    }

    static [void] Failure([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::Failure, $Message)
        ++[Log]::_failureCount
    }

    static [void] BeginSection([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::BeginSection, $Message)
    }

    static [void] EndSection([string] $Message) {
        [Log]::DispatchMessage([LogMessageType]::EndSection, $Message)
    }

    static hidden [void] DispatchMessage([LogMessageType] $MessageType, [string] $Message) {
        foreach ($l in [Log]::Listeners) { $l.ProcessMessage($MessageType, $Message) }
    }

    static [uint32] WarningCount() { return [Log]::_warningCount }
    static [uint32] ErrorCount() { return [Log]::_errorCount }
    static [uint32] SuccessCount() { return [Log]::_successCount }
    static [uint32] FailureCount() { return [Log]::_failureCount }

    static [void] Reset() {
        [Log]::_errorCount = 0
        [Log]::_warningCount = 0
        [Log]::_successCount = 0
        [Log]::_failureCount = 0
    }

    # ArrayList over standard array for mutability
    static [System.Collections.ArrayList] $Listeners = [System.Collections.ArrayList]::new()

    static hidden [uint32] $_warningCount = 0
    static hidden [uint32] $_errorCount = 0
    static hidden [uint32] $_successCount = 0
    static hidden [uint32] $_failureCount = 0
}

class LogListenerBase {
    [void] ProcessMessage([LogMessageType] $MessageType, [string] $Message) {
        throw "calling abstract message"
    }
}

class LogObserver : LogListenerBase{
    LogObserver([scriptblock] $onProcessMessage) {
        $this.OnProcessMessage = $onProcessMessage
    }

    [void] ProcessMessage([LogMessageType] $MessageType, [string] $Message) {
        $this.OnProcessMessage.Invoke($MessageType, $Message)
    }

    hidden [scriptblock] $OnProcessMessage
}

class FileLogListener : LogListenerBase {
    FileLogListener([string] $fileName) {
        $this._fileName = $fileName
    }

    [void] ProcessMessage([LogMessageType] $messageType, [string] $message) {
        if ($messageType -eq [LogMessageType]::EndSection) {
            if ($this._nestingLevel -eq 0) { throw "mismatching log sections" }
            --$this._nestingLevel
        }

        Write-Output ("{0,-14} {1}{2}: {3}" -f
            (Get-Date).ToString("HH:mm:ss.FFFFF"),
            <# indent #> (" " * 4 * $this._nestingLevel),
            [FileLogListener]::GetMessageTypeString($messageType),
            $message
        ) >> $this._fileName

        if ($messageType -eq [LogMessageType]::BeginSection) { ++$this._nestingLevel }
    }

    static hidden [string] GetMessageTypeString([LogMessageType] $messageType) {
        switch ($messageType) {
            ([LogMessageType]::Comment) { return "COMMENT" }
            ([LogMessageType]::Warning) { return "WARNING" }
            ([LogMessageType]::Error) { return "ERROR" }
            ([LogMessageType]::Success) { return "SUCCESS" }
            ([LogMessageType]::Failure) { return "FAILURE" }
            ([LogMessageType]::BeginSection) { return "BEGIN" }
            ([LogMessageType]::EndSection) { return "END" }
        }
        throw "unknown message type '$messageType'"
    }

    hidden [string] $_fileName
    hidden [uint32] $_nestingLevel = 0
}

class ConsoleLogListener : LogListenerBase {
    [void] ProcessMessage([LogMessageType] $MessageType, [string] $Message) {
        switch ($MessageType) {
            ([LogMessageType]::Warning) {
                Write-Host -ForegroundColor Yellow $Message
            }

            ([LogMessageType]::Error) {
                Write-Host -ForegroundColor Red $Message
            }

            ([LogMessageType]::Success) {
                Write-Host -ForegroundColor Green $Message
            }

            ([LogMessageType]::Failure) {
                Write-Host -ForegroundColor Red $Message
            }
        }
    }
}

class LogInterceptor : LogListenerBase {

    LogInterceptor([scriptblock] $onProcessMessage) {
        $this.onProcessMessage = $onProcessMessage
        $this.listeners = [Log]::Listeners
        [Log]::Listeners = [System.Collections.ArrayList]::new(@($this))
    }

    [void] ProcessMessage([LogMessageType] $MessageType, [string] $Message) {
        $this.onProcessMessage.Invoke($this, $MessageType, $Message)
    }

    [void] DispatchMessage([LogMessageType] $MessageType, [string] $Message) {
        foreach ($l in $this.listeners) { $l.ProcessMessage($MessageType, $Message) }
    }

    [void] Dispose() {
        [Log]::Listeners = $this.listeners
    }

    hidden [System.Collections.ArrayList] $listeners
    hidden [scriptblock] $onProcessMessage
}