timetagger-ps.psm1

#Requires -Version 7.2
using module ./classes/activity.psm1
using module ./classes/timetagger.psm1

$ErrorActionPreference = 'Stop'
$script:TimeTaggerWrapper = [TimeTaggerWrapper]::new()
[ValidateSet('Pre', 'Post')]
$Script:TagPosition = 'Pre'

# get the date time truncated to seconds
function GetDate {
    $Now = Get-Date 
    $Now = $Now.AddTicks( - ($Now.Ticks % [timespan]::TicksPerSecond))
    $Now
}

function GetDescription {
    param(
        [string[]]$Tags,
        [string]$Description,
        [ValidateSet('Pre', 'Post')]
        [string]$TagPosition = 'Pre'
    )

    $Ret = $TagPosition -eq 'Pre' ? 
        ($Tags + $Description) -join ' ' :
        (, $Description + $Tags) -join ' '
       
    $Ret
}

function IsValidTag() {
    param (
        [String]$Tag
    )

    $Ret = $Tag -match '^#[a-z0-9][a-z0-9-]*[a-z0-9]$'

    return $Ret
}

function Add-Activity {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ 
            IsValidTag($_) 
        })]
        [string[]]$Tags,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string]$Description,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('From')]
        [datetime]$StartedAt,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('To')]
        [ValidateScript({ 
            $_ -gt $StartedAt
         })]
        [datetime]$EndedAt

    )

    $Description = GetDescription -Tags $Tags -Description $Description -TagPosition $Script:TagPosition
    $Activity = [Activity]::new($StartedAt, $EndedAt, $Description)
    
    Write-Verbose "Adding Activity: $Description"

    return $script:TimeTaggerWrapper.PutActivity($Activity)
}

function Get-Activity {
    [CmdletBinding(DefaultParameterSetName = 'ByDate')]
    param (
        [Parameter(ParameterSetName = 'ByDate')]
        [datetime]$From = (Get-Date).Date,

        [Parameter(ParameterSetName = 'ByDate')]
        [datetime]$To = (Get-Date).Date.AddDays(1),

        [Parameter(Mandatory = $true, ParameterSetName = "ByPeriod")]
        [ValidateSet('Today', 'Yesterday', 'ThisWeek', 'ThisMonth')]
        [string]$Period,

        [Parameter(ParameterSetName = "ByDate")]
        [Parameter(ParameterSetName = "ByPeriod")]
        [ValidateSet('Active', 'Stopped', 'All')]
        [string]$State = 'Active',

        [Parameter(ParameterSetName = "ByDate")]
        [Parameter(ParameterSetName = "ByPeriod")]
        [ValidateSet('Hidden', 'NotHidden', 'All')]
        [string]$Visibility = 'NotHidden'
    )

    if ($PSCmdlet.ParameterSetName -eq 'ByPeriod') {
        $Today = (Get-Date).Date 

        switch ($Period) {
            'Today' { 
                $From = $Today
                $To = $From.AddDays(1)
            }
            'Yesterday' { 
                $From = $Today.AddDays(-1)
                $To = $From.AddDays(1)
            }
            'ThisWeek' {
                # monday is first day of week
                $From = $Today.AddDays( - ($Today.DayOfWeek - 1))
                $To = $Today.AddDays(1)
            }
            'ThisYear' {
                # from Jan 01
                $From = [datetime]::new((Get-Date).Year, 1, 1)
                $To = $Today.AddDays(1)
            }
            Default {
                throw "Unknown Period"
            }
        }
    }

    $Activities = $script:TimeTaggerWrapper.GetActivities($From, $To)
    $Ret = @()

    foreach ($Activity in $Activities) {
        $Include = $true

        switch ($State) {
            'All' { $Include = $true }
            'Active' { $Include = $Activity.IsActive }
            'Stopped' { $Include = !$Activity.IsActive }
            Default {
                throw "Unexpected state in Get-Activity: $State"
            }
        }
        if ($Include) {
            switch ($Visibility) {
                'All' { $Include = $true }
                'Hidden' { $Include = $Activity.IsHidden }
                'NotHidden' { $Include = !$Activity.IsHidden }
                Default {
                    throw "Unexpected visibility in Get-Activity: $Visibility"
                }
            }
        }
        
        if ($Include) {
            $Ret += $Activity
        }
    }

    return $Ret
}


function Hide-Activity {
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByActivity', ValueFromPipeline = $true, Position = 0)]
        [Activity]$Activity
    )
    
    begin {
        Write-Verbose "$($MyInvocation.MyCommand): begin"

        $outBuffer = $null
        
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
            $PSBoundParameters['OutBuffer'] = 1
        }
        
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand("$($MyInvocation.MyCommand.ModuleName)\Set-Activity", [System.Management.Automation.CommandTypes]::Function)
        $scriptCmd = { & $wrappedCmd @PSBoundParameters }

        $Pipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $Pipeline.Begin($PSCmdlet) 
    }
    
    process {
        $_.Hide()
        $Pipeline.Process($_)
    }
    
    end {
        Write-Verbose "$($MyInvocation.MyCommand): end"
    }
    
    clean {

    }
}

function Open-TimeTaggerApp {
    [CmdletBinding()]
    param (
    )

    Start-Process -FilePath 'https://timetagger.app/app/'
}

function Set-TimeTaggerAccount {
    param(
        [string]$ApiToken,
        [switch]$Persist
    )

    $Script:ApiToken = $ApiToken    

    $Password = ConvertTo-SecureString $ApiToken -AsPlainText -Force
    $Cred = New-Object "System.Management.Automation.PSCredential" ('<not-used>', $Password)
    
    $Cred | Export-Clixml -Path ~/.timetagger
}

function Set-Activity {
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByActivity', ValueFromPipeline = $true, Position = 0)]
        [Activity]$Activity
    )

    begin {
        Write-Verbose 'Set-Activity: begin'
    }

    process {
        $script:TimeTaggerWrapper.PutActivity($_)
    }

    end {
        Write-Verbose 'Set-Activity: end'
    }

    clean {}
    
}

function Show-Activity {
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByActivity', ValueFromPipeline = $true, Position = 0)]
        [Activity]$Activity
    )
    
    begin {
        Write-Verbose "$($MyInvocation.MyCommand): begin"

        $outBuffer = $null
        
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
            $PSBoundParameters['OutBuffer'] = 1
        }
        
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand("$($MyInvocation.MyCommand.ModuleName)\Set-Activity", [System.Management.Automation.CommandTypes]::Function)
        $scriptCmd = { & $wrappedCmd @PSBoundParameters }

        $Pipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $Pipeline.Begin($PSCmdlet) 
    }
    
    process {
        if ($_.IsHidden) {
            $_.Unhide()
            $Pipeline.Process($_)
        }
        else {
            Write-Warning "Activity (Key = $($Activity.Key) is not hidden)"
        }
    }
    
    end {
        Write-Verbose "$($MyInvocation.MyCommand): end"
        
    }
    
    clean {
        
    }
}

function Split-Activity {
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByActivity', ValueFromPipeline = $true, Position = 0)]
        [Activity]$Activity,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByActivity', ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({ $SplitFrom -gt $Activity.StartedAt })]
        [datetime]$SplitFrom,

        [Parameter(Mandatory = $false, ParameterSetName = 'ByActivity', ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({ $NewActivityStartedAt -ge $SplitFrom })]
        [datetime]$NewActivityStartedAt = $SplitFrom,

        [Parameter(Mandatory = $false, ParameterSetName = 'ByActivity', ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({ $NewActivityEndedAt -gt $NewActivityStartedAt })]
        [nullable[datetime]]$NewActivityEndedAt

    )

    $Activity.EndedAt = $SplitFrom
    Set-Activity -Activity $Activity

    $Params = @{
        Tags        = $Activity.Tags 
        Description = $Activity.Description
        StartedAt   = $NewActivityStartedAt
    }
        
    if ($null -eq $NewActivityEndedAt) {
        Start-Activity @Params 
    }
    else {
        Add-Activity @Params -EndedAt $NewActivityEndedAt
    }
}

function Start-Activity {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ IsValidTag($_) })]
        [string[]]$Tags,

        [string]$Description,

        [Alias('From')]
        [datetime]$StartedAt = (Get-Date).ToUniversalTime(),

        [switch]$IgnoreRunning
    )

    if (!$IgnoreRunning.IsPresent) {
        Get-Activity -State 'Active' -From $StartedAt | Stop-Activity
    }

    $Description = GetDescription -Tags $Tags -Description $Description -TagPosition $Script:TagPosition
    $Activity = [Activity]::new($StartedAt, $Description)
    
    Write-Verbose "Starting Activity: $Description"

    return $script:TimeTaggerWrapper.PutActivity($Activity)
}

function Stop-Activity {
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByActivity', ValueFromPipeline = $true, Position = 0)]
        [Activity]$Activity
    )

    begin {
        Write-Verbose 'Stop-Activity: begin'

        $outBuffer = $null
        
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
            $PSBoundParameters['OutBuffer'] = 1
        }
        
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand("$($MyInvocation.MyCommand.ModuleName)\Set-Activity", [System.Management.Automation.CommandTypes]::Function)
        $scriptCmd = { & $wrappedCmd @PSBoundParameters }

        $Pipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $Pipeline.Begin($PSCmdlet) 
    }

    process {
        $_.EndedAt = GetDate
        $Pipeline.Process($_)    
    }

    end {
        Write-Verbose 'Stop-Activity: end'
    }

    clean {}
    
}