ghcups.psm1

Set-StrictMode -Version Latest

# Constant

Set-Variable systemGlobalDataPath -Option Constant -Value "$Env:ProgramData\ghcups"
Set-Variable userGlobalDataPath -Option Constant -Value "$Env:APPDATA\ghcups"
Set-Variable versionPattern -Option Constant -Value '[0-9]+(\.[0-9]+)*'
Set-Variable ghcPathPattern -Option Constant -Value ([Regex]::Escape($Env:ChocolateyInstall) + '\\lib\\ghc\.' + $versionPattern + '\\tools\\ghc-' + $versionPattern + '\\bin')
Set-Variable cabalPathPattern -Option Constant -Value ([Regex]::Escape($Env:ChocolateyInstall) + '\\lib\\cabal\.' + $versionPattern + '\\tools\\cabal-' + $versionPattern)
Set-Variable localConfigName -Option Constant -Value 'ghcups.yaml'
Set-Variable globalConfigName -Option Constant -Value 'config.yaml'

# Common

function Find-LocalConfigPath {
    param (
        [Parameter(Mandatory)][String] $Dir
    )

    While ($true) {
        if ($Dir -eq $Env:USERPROFILE -or [String]::IsNullOrEmpty($Dir)) {
            ''
            return
        }
        $test = Join-Path $Dir $localConfigName
        if (Test-Path $test) {
            $test
            return
        }
        $Dir = Split-Path $Dir -Parent
    }
}

function Get-Config {
    param (
        [Parameter(Mandatory)][AllowNull()][AllowEmptyString()][String] $Path
    )

    if ([String]::IsNullOrEmpty($Path)) {
        $null
        return
    }
    if (-not (Test-Path $Path)) {
        $null
        return
    }
    ConvertFrom-Yaml (Get-Content $Path -Raw)
}

function Get-HashtaleItem {
    param (
        [Parameter(Mandatory)][Object[]] $Name,
        [Hashtable] $Hashtable
    )

    $item = $Hashtable
    foreach ($n in $Name) {
        if ($null -eq $item) {
            $null
            return
        }
        $item = $item[$n]
    }
    $item
}

function Copy-HashtableDeeply {
    param (
        [Hashtable] $Hashtable
    )

    $result = @{}
    foreach ($key in $Hashtable.Keys) {
        $item = $Hashtable[$key]
        if ($item -is [Hashtable]) {
            $result.Add($key, (Copy-HashtableDeeply $item))
            continue
        }
        if ($null -eq $item) {
            $result.Add($key, $null)
            continue
        }
        $result.Add($key, $item.Clone())
    }
    $result
}

function Join-Hashtables {
    param (
        [Hashtable[]] $Hashtables,
        [Switch] $Breaking = $false
    )

    if ($null -eq $Hashtables -or @() -eq $Hashtables) {
        $null
        return
    }

    $result = $null
    foreach ($h in $Hashtables) {
        if ($null -eq $h) {
            continue
        }
        if ($null -eq $result) {
            if ($Breaking) {
                $result = $h
            }
            else {
                $result = Copy-HashtableDeeply $h
            }
            continue
        }
        foreach ($key in $h.Keys) {
            $value = $h[$key]
            if ($result.ContainsKey($key) -and $result -is [Hashtable] -and $value -is [Hashtable]) {
                [void] (Join-Hashtables $result[$key], $value -Breaking)
            }
            else {
                $result.Add($key, $h[$key])
            }
        }
    }
    $result
}

function All {
    param ([Parameter(ValueFromPipeline)][Boolean[]] $ps)
    begin { $acc = $true }
    process { foreach ($p in $ps) { $acc = $acc -and $p } }
    end { $acc }
}

function Set-PathEnv {
    param (
        [Parameter(Mandatory)][String[]] $patterns,
        [Parameter(Mandatory)][AllowEmptyString()][String] $path
    )

    if (-not [String]::IsNullOrEmpty($path) -and -not (Test-Path $path)) {
        Write-Warning "`"$path`" is not an existing path"
    }
    $restPaths = $Env:Path -split ';' | Where-Object { $v = $_; $patterns | ForEach-Object { $v -notmatch $_ } | All }
    $newPaths = ,$path + $restPaths | Where-Object { -not [String]::IsNullOrEmpty($_) }
    Set-Item Env:\Path -Value ($newPaths -join ';')
}

function Get-InstalledChocoItems {
    param (
        [Parameter(Mandatory)][String] $App
    )

    $path = "$Env:ChocolateyInstall\lib\$App."
    Get-Item "$path*" | ForEach-Object { ([String]$_).Remove(0, $path.Length) }
}

# .SYNOPSIS
# Creats the ghcups.yaml with the default contents.
function Write-GhcupsConfigTemplate {
    param (
        [String] $Path = '.'
    )

    "# The key is the name you want, the value is the path of directory which contains ghc, ghci, etc.`nghc: {}`n`n# The same with ghc for cabal.`ncabal: {}" | Out-File (Join-Path $Path $localConfigName) -NoClobber
}

function Get-ExePathsFromConfigs {
    param (
        [Hashtable[]] $Configs,
        [String] $name
    )

    $patterns = `
      $Configs | `
      ForEach-Object { Get-HashtaleItem $name $_ } | `
      Where-Object { $null -ne $_ } | `
      ForEach-Object -begin { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseDeclaredVarsMoreThanAssignments', 'paths')] $paths = @() } -process { $paths += $_.Values } -end { $paths } | `
      Where-Object { -not [String]::IsNullOrEmpty($_) }
    if ($null -eq $patterns) {
        @()
    }
    $patterns
}

function Start-Choco {
    try {
        choco @Args
    }
    catch [System.Management.Automation.CommandNotFoundException] {
        $choice = Read-Host '"choco" is not found. Will you install Chocoratey? [y/N]'
        if ('y' -ne $choice) {
            return
        }
        Install-Choco
    }
}

# .SYNOPSIS
# Install the Chocolatey.
function Install-Choco {
    if ((New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
       # with administrative privileges
        Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
        return
    }
    Write-Host 'Installing Chocolatey...'
    $logFile = "$Env:TEMP\ghcups.log"
    Start-process `
        -FilePath powershell `
        -ArgumentList "-Command & { Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) | Tee-Object $logFile }" `
        -Verb RunAs `
        -Wait
    Write-Host "Log file is at `"$logFile`""
    Write-Host 'Update Env:\Path or restart the PowerShell'
}

# GHC

function Get-ChocoGhc {
    param (
        [Parameter(Mandatory)][String] $Ghc
    )

    "$Env:ChocolateyInstall\lib\ghc.$Ghc\tools\ghc-$Ghc\bin"
}

# .SYNOPSIS
# Sets the version or variant of GHC to the Path environment variable of the current session.
function Set-Ghc {
    param (
        [Parameter(Mandatory)][String] $Ghc
    )

    $localConfig = Get-Config (Find-LocalConfigPath (Get-Location))
    $userGlobalConfig = Get-Config (Join-Path $userGlobalDataPath $globalConfigName)
    $systemGlobalConfig = Get-Config (Join-Path $systemGlobalDataPath $globalConfigName)
    [Hashtable] $cs = Join-Hashtables $localConfig, $userGlobalConfig, $systemGlobalConfig
    $ghcDir = Get-HashtaleItem -Name 'ghc', $Ghc -Hashtable $cs
    if ([String]::IsNullOrEmpty($ghcDir)) {
        if ($Ghc -notmatch ('\A' + $versionPattern + '\Z')) {
            Write-Error "No sutch GHC: $Ghc"
            return
        }
        $ghcDir = Get-ChocoGhc $Ghc
    }
    $patterns = Get-ExePathsFromConfigs $localConfig, $userGlobalConfig, $systemGlobalConfig 'ghc' | ForEach-Object { '\A' + [Regex]::Escape($_) + '\Z' }
    $patterns += $ghcPathPattern
    Set-PathEnv $patterns $ghcDir
}

# .SYNOPSIS
# Removes all GHC values from the Path environment variable of the current session.
function Clear-Ghc {
    $localConfig = Get-Config (Find-LocalConfigPath (Get-Location))
    $userGlobalConfig = Get-Config (Join-Path $userGlobalDataPath $globalConfigName)
    $systemGlobalConfig = Get-Config (Join-Path $systemGlobalDataPath $globalConfigName)
    $patterns = Get-ExePathsFromConfigs $localConfig, $userGlobalConfig, $systemGlobalConfig 'ghc' | ForEach-Object { '\A' + [Regex]::Escape($_) + '\Z' }
    $patterns += $ghcPathPattern
    Set-PathEnv $patterns $null
}

# .SYNOPSIS
# Installs the specified GHC with the Chocolatey.
function Install-Ghc {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', 'Start-Choco')]
    param (
        [Parameter(Mandatory)][String] $Ghc,
        [Switch] $Set = $false
    )

    Start-Choco install ghc --version $Ghc --side-by-side

    if ($Set) {
        Set-Ghc -Ghc $Ghc
    }
}

# .SYNOPSIS
# Uninstalls the specified GHC with the Chocolatey.
function Uninstall-Ghc {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', 'Start-Choco')]
    param (
        [Parameter(Mandatory)][String] $Ghc
    )

    Start-Choco uninstall ghc --version $Ghc
}

# .SYNOPSIS
# Shows the GHCs which is specified by the ghcups.yaml and config.yaml, which is installed by the Chocolatey and which is hosted on the Chocolatey repository.
function Show-Ghc {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', 'Start-Choco')]
    param ()

    $localConfigPath = Find-LocalConfigPath (Get-Location)
    $localConfigDir = $null
    if (-not [String]::IsNullOrEmpty($localConfigPath)) {
        $localConfigDir = Split-Path $localConfigPath -Parent
    }
    $localConfig = Get-Config $localConfigPath
    $userGlobalConfig = Get-Config (Join-Path $userGlobalDataPath $globalConfigName)
    $systemGlobalConfig = Get-Config (Join-Path $systemGlobalDataPath $globalConfigName)
    $config = Join-Hashtables $localConfig, $userGlobalConfig, $systemGlobalConfig
    $span = $false
    $ghcs = Get-HashtaleItem 'ghc' $config
    if ($null -ne $ghcs -and 0 -lt $ghcs.Count) {
        Write-Output "$localConfigName ($(($localConfigDir, $userGlobalDataPath, $systemGlobalDataPath | Where-Object { $null -ne $_ }) -Join ', '))"
        foreach ($k in $config.ghc.Keys) {
            Write-Output " ${k}: $($config.ghc[$k])"
        }
        $span = $true
    }
    $chocoGhcs = Get-InstalledChocoItems 'ghc'
    if ($null -ne $chocoGhcs) {
        if ($span) {
            Write-Output ''
        }
        Write-Output 'Chocolatey (Installed)'
        foreach ($g in $chocoGhcs) {
            Write-Output " ${g}: $Env:ChocolateyInstall\lib\ghc.$g\tools\ghc-$g\bin"
        }
        $span = $true
    }
    if ($span) {
        Write-Output ''
    }
    Write-Output 'Chocolatey (Remote)'
    Start-Choco list ghc --by-id-only --all-versions | ForEach-Object { " $_" }
}

# Cabal

function Get-ChocoCabal {
    param (
        [Parameter(Mandatory)][String] $Cabal
    )

    "$Env:ChocolateyInstall\lib\cabal.$Cabal\tools\cabal-$Cabal"
}

# .SYNOPSIS
# Sets the version or variant of Cabal to the Path environment variable of the current session.
function Set-Cabal {
    param (
        [Parameter(Mandatory)][String] $Cabal
    )

    $localConfig = Get-Config (Find-LocalConfigPath (Get-Location))
    $userGlobalConfig = Get-Config (Join-Path $userGlobalDataPath $globalConfigName)
    $systemGlobalConfig = Get-Config (Join-Path $systemGlobalDataPath $globalConfigName)
    $cabalDir = Get-HashtaleItem -Name 'cabal', $Cabal -Hashtable (Join-Hashtables $localConfig, $userGlobalConfig, $systemGlobalConfig)
    if ([String]::IsNullOrEmpty($cabalDir)) {
        if ($Cabal -notmatch ('\A' + $versionPattern + '\Z')) {
            Write-Error "No sutch Cabal: $Cabal"
            return
        }
        $cabalDir = Get-ChocoCabal $Cabal
    }
    $patterns = Get-ExePathsFromConfigs $localConfig, $userGlobalConfig, $systemGlobalConfig 'cabal' | ForEach-Object { '\A' + [Regex]::Escape($_) + '\Z' }
    $patterns += $cabalPathPattern
    Set-PathEnv $patterns $cabalDir
}

# .SYNOPSIS
# Removes all Cabal values from the Path environment variable of the current session.
function Clear-Cabal {
    $localConfig = Get-Config (Find-LocalConfigPath (Get-Location))
    $userGlobalConfig = Get-Config (Join-Path $userGlobalDataPath $globalConfigName)
    $systemGlobalConfig = Get-Config (Join-Path $systemGlobalDataPath $globalConfigName)
    $patterns = Get-ExePathsFromConfigs $localConfig, $userGlobalConfig, $systemGlobalConfig 'cabal' | ForEach-Object { '\A' + [Regex]::Escape($_) + '\Z' }
    $patterns += $cabalPathPattern
    Set-PathEnv $patterns $null
}

# .SYNOPSIS
# Installs the specified Cabal with the Chocolatey.
function Install-Cabal {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', 'Start-Choco')]
    param (
        [Parameter(Mandatory)][String] $Cabal,
        [Switch] $Set = $false
    )

    Start-Choco install cabal --version $Cabal --side-by-side

    if ($Set) {
        Set-Cabal -Cabal $Cabal -Method $Method
    }
}

# .SYNOPSIS
# Uninstalls the specified Cabal with the Chocolatey.
function Uninstall-Cabal {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', 'Start-Choco')]
    param (
        [Parameter(Mandatory)][String] $Cabal
    )

    Start-Choco uninstall cabal --version $Cabal
}

# .SYNOPSIS
# Shows the Cabals which is specified by the ghcups.yaml and config.yaml, which is installed by the Chocolatey and which is hosted on the Chocolatey repository.
function Show-Cabal {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', 'Start-Choco')]
    param ()

    $localConfigPath = Find-LocalConfigPath (Get-Location)
    $localConfigDir = $null
    if (-not [String]::IsNullOrEmpty($localConfigPath)) {
        $localConfigDir = Split-Path $localConfigPath -Parent
    }
    $localConfig = Get-Config $localConfigPath
    $userGlobalConfig = Get-Config (Join-Path $userGlobalDataPath $globalConfigName)
    $systemGlobalConfig = Get-Config (Join-Path $systemGlobalDataPath $globalConfigName)
    $config = Join-Hashtables $localConfig, $userGlobalConfig, $systemGlobalConfig
    $span = $false
    $cabals = Get-HashtaleItem 'cabal' $config
    if ($null -ne $cabals -and 0 -lt $cabals.Count) {
        Write-Output "$localConfigName ($(($localConfigDir, $userGlobalDataPath, $systemGlobalDataPath | Where-Object { $null -ne $_ }) -Join ', '))"
        foreach ($k in $config.cabal.Keys) {
            Write-Output " ${k}: $($config.cabal[$k])"
        }
        $span = $true
    }
    $chocoCabals = Get-InstalledChocoItems 'cabal'
    if ($null -ne $chocoCabals) {
        if ($span) {
            Write-Output ''
        }
        Write-Output 'Chocolatey (Installed)'
        foreach ($g in $chocoCabals) {
            Write-Output " ${g}: $Env:ChocolateyInstall\lib\cabal.$g\tools\cabal-$g\bin"
        }
        $span = $true
    }
    if ($span) {
        Write-Output ''
    }
    Write-Output 'Chocolatey (Remote)'
    Start-Choco list cabal --by-id-only --all-versions | ForEach-Object { " $_" }
}

# Export

Export-ModuleMember `
    -Function `
        'Set-Ghc', `
        'Clear-Ghc', `
        'Install-Ghc', `
        'Uninstall-Ghc', `
        'Show-Ghc', `
        'Set-Cabal', `
        'Clear-Cabal', `
        'Install-Cabal', `
        'Uninstall-Cabal', `
        'Show-Cabal', `
        'Write-GhcupsConfigTemplate', `
        'Install-Choco'