src/functions/settings.ps1

$script:ConfigMapSettings = $null

function New-ConfigMapSettings {
    param(
        $BaseSettings,
        [System.Collections.IDictionary]$Overrides = @{}
    )

    $settings = @{
        Debug          = $false
        Concurrently   = $false
        TmuxAutoWindow = $false
        DiscoveryCache = $true
        Process        = 'qbuild'
    }

    foreach ($propertyName in @($settings.Keys)) {
        $envPath = "env:QCONF_$propertyName"
        if (Test-Path -Path $envPath) {
            $settings[$propertyName] = (Get-Item -Path $envPath).Value
        }
    }

    if ($BaseSettings) {
        foreach ($propertyName in @($settings.Keys)) {
            $settings[$propertyName] = $BaseSettings.PSObject.Properties[$propertyName].Value
        }
    }

    foreach ($override in $Overrides.GetEnumerator()) {
        if (-not $settings.ContainsKey($override.Key)) {
            throw "Unknown ConfigMap setting '$($override.Key)'."
        }

        $settings[$override.Key] = $override.Value
    }

    $settingsObject = [pscustomobject]$settings
    $settingsObject.PSObject.TypeNames.Insert(0, 'ConfigMap.Settings')

    return $settingsObject
}

function Update-ConfigMapSettings {
    $script:ConfigMapSettings = New-ConfigMapSettings

    return $script:ConfigMapSettings
}

function Enter-ConfigMapSettingsScope {
    param(
        [System.Collections.IDictionary]$Settings
    )

    $previousSettings = $script:ConfigMapSettings
    if ($Settings) {
        $script:ConfigMapSettings = New-ConfigMapSettings -BaseSettings $previousSettings -Overrides $Settings
    }

    return $previousSettings
}

function Exit-ConfigMapSettingsScope {
    param($PreviousSettings)

    $script:ConfigMapSettings = $PreviousSettings
}

function New-ConfigMapOperationContext {
    return @{
        IncludeCache    = @{}
        LoadingIncludes = @{}
        Dependencies    = @{}
    }
}

function Find-ConfigMapAncestorSettings {
    param(
        [System.Collections.IDictionary]$Node,
        [string[]]$Segments,
        [switch]$IncludeTarget,
        [System.Collections.Generic.HashSet[string]]$Visited,
        [hashtable]$Cache,
        [hashtable]$Loading
    )

    $notFound = [pscustomobject]@{ Found = $false; Settings = $null }
    if ($null -eq $Segments -or $Segments.Count -eq 0 -or $null -eq $Node) {
        return $notFound
    }

    $segment = $Segments[0]
    $rest = if ($Segments.Count -gt 1) { $Segments[1..($Segments.Count - 1)] } else { @() }
    $isLast = $rest.Count -eq 0
    $visitKey = '{0}|{1}' -f [System.Runtime.CompilerServices.RuntimeHelpers]::GetHashCode($Node), $segment
    if (-not $Visited.Add($visitKey)) {
        return $notFound
    }

    $baseDir = $Node._baseDir
    $list = $Node
    if ($Node.list) {
        $list = $Node.list
    }
    if ($list -isnot [System.Collections.IDictionary]) {
        return $notFound
    }

    if ($list.Contains($segment)) {
        $settings = [System.Collections.Generic.List[object]]::new()
        $child = $list[$segment]
        $apply = (-not $isLast) -or $IncludeTarget
        if ($apply -and $child -is [System.Collections.IDictionary] -and $child._settings) {
            $settings.Add($child._settings)
        }
        if (-not $isLast -and $child -is [System.Collections.IDictionary]) {
            $inner = Find-ConfigMapAncestorSettings -Node $child -Segments $rest -IncludeTarget:$IncludeTarget -Visited $Visited -Cache $Cache -Loading $Loading
            if ($inner.Found) {
                foreach ($item in $inner.Settings) {
                    $settings.Add($item)
                }
            }
        }
        return [pscustomobject]@{ Found = $true; Settings = $settings }
    }

    $includes = $null
    if ($list.Contains('#include')) {
        $includes = $list['#include']
    }
    if ($includes -isnot [System.Collections.IDictionary]) {
        return $notFound
    }

    foreach ($inc in @($includes.GetEnumerator())) {
        $usePrefix = $inc.Value -is [System.Collections.IDictionary] -and $inc.Value.prefix -eq $true
        if (-not $usePrefix -or $inc.Key -ne $segment) {
            continue
        }

        $included = Import-IncludedConfigMap -DirectoryName "$($inc.Key)" -BaseDir $baseDir -Cache $Cache -Loading $Loading
        if (-not $included) {
            continue
        }

        if ($isLast) {
            $settings = [System.Collections.Generic.List[object]]::new()
            if ($IncludeTarget -and $included._settings) {
                $settings.Add($included._settings)
            }
            return [pscustomobject]@{ Found = $true; Settings = $settings }
        }

        $inner = Find-ConfigMapAncestorSettings -Node $included -Segments $rest -IncludeTarget:$IncludeTarget -Visited $Visited -Cache $Cache -Loading $Loading
        if (-not $inner.Found) {
            continue
        }

        $settings = [System.Collections.Generic.List[object]]::new()
        if ($included._settings) {
            $settings.Add($included._settings)
        }
        foreach ($item in $inner.Settings) {
            $settings.Add($item)
        }
        return [pscustomobject]@{ Found = $true; Settings = $settings }
    }

    foreach ($inc in @($includes.GetEnumerator())) {
        $usePrefix = $inc.Value -is [System.Collections.IDictionary] -and $inc.Value.prefix -eq $true
        if ($usePrefix) {
            continue
        }

        $included = Import-IncludedConfigMap -DirectoryName "$($inc.Key)" -BaseDir $baseDir -Cache $Cache -Loading $Loading
        if (-not $included) {
            continue
        }

        $inner = Find-ConfigMapAncestorSettings -Node $included -Segments $Segments -IncludeTarget:$IncludeTarget -Visited $Visited -Cache $Cache -Loading $Loading
        if (-not $inner.Found) {
            continue
        }

        $settings = [System.Collections.Generic.List[object]]::new()
        if ($included._settings) {
            $settings.Add($included._settings)
        }
        foreach ($item in $inner.Settings) {
            $settings.Add($item)
        }
        return [pscustomobject]@{ Found = $true; Settings = $settings }
    }

    return $notFound
}

function Enter-ConfigMapAncestorSettingsScopes {
    param(
        [System.Collections.IDictionary]$Map,
        [string]$EntryKey,
        [switch]$IncludeTarget,
        [hashtable]$OperationContext
    )

    $entered = [System.Collections.Generic.List[object]]::new()
    try {
        $segments = @($EntryKey -split '\.')
        $visited = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
        $includeCache = if ($OperationContext) { $OperationContext.IncludeCache } else { @{} }
        $loadingIncludes = if ($OperationContext) { $OperationContext.LoadingIncludes } else { @{} }
        $found = Find-ConfigMapAncestorSettings -Node $Map -Segments $segments -IncludeTarget:$IncludeTarget -Visited $visited -Cache $includeCache -Loading $loadingIncludes
        if ($found.Found) {
            foreach ($settings in $found.Settings) {
                $entered.Add((Enter-ConfigMapSettingsScope -Settings $settings))
            }
        }
    }
    catch {
        Exit-ConfigMapSettingsScopes -Scopes $entered
        throw
    }

    foreach ($scope in $entered) {
        ,$scope
    }
}

function Invoke-WithEntrySettings {
    param(
        [System.Collections.IDictionary]$Map,
        [string]$EntryKey,
        $Entry,
        [hashtable]$OperationContext,
        [Parameter(Mandatory)]
        [scriptblock]$ScriptBlock
    )

    # Parameter names would hide the caller's $map and $entry from the scriptblock.
    $settingsMap = $Map
    $settingsEntryKey = $EntryKey
    $settingsEntry = $Entry
    $settingsScript = $ScriptBlock
    $settingsOperationContext = $OperationContext
    Remove-Variable Map, EntryKey, Entry, OperationContext, ScriptBlock -ErrorAction SilentlyContinue

    $scopes = [System.Collections.Generic.List[object]]::new()
    try {
        $mapSettings = $null
        if ($settingsMap -is [System.Collections.IDictionary]) {
            $mapSettings = $settingsMap._settings
        }
        $scopes.Add((Enter-ConfigMapSettingsScope -Settings $mapSettings))

        if (-not [string]::IsNullOrEmpty($settingsEntryKey)) {
            $ancestorScopes = @(Enter-ConfigMapAncestorSettingsScopes -Map $settingsMap -EntryKey $settingsEntryKey -OperationContext $settingsOperationContext)
            foreach ($ancestorScope in $ancestorScopes) {
                $scopes.Add($ancestorScope)
            }
        }

        $leafSettings = $null
        if ($settingsEntry -is [System.Collections.IDictionary]) {
            $leafSettings = $settingsEntry._settings
        }
        $scopes.Add((Enter-ConfigMapSettingsScope -Settings $leafSettings))

        & $settingsScript
    }
    finally {
        Exit-ConfigMapSettingsScopes -Scopes $scopes
    }
}

function Exit-ConfigMapSettingsScopes {
    param($Scopes)

    for ($index = $Scopes.Count - 1; $index -ge 0; $index--) {
        Exit-ConfigMapSettingsScope -PreviousSettings $Scopes[$index]
    }
}

function Get-ConfigMapSettings {
    return $script:ConfigMapSettings
}

function Get-ConfigMapSettingsForPath {
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Map,
        [string]$Path,
        [hashtable]$OperationContext
    )

    $operationContext = if ($OperationContext) { $OperationContext } else { New-ConfigMapOperationContext }

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return Invoke-WithEntrySettings -Map $Map -EntryKey '' -Entry $null -OperationContext $operationContext -ScriptBlock {
            Get-ConfigMapSettings
        }
    }

    # Match qbuild: foo.all inspects the parent entry's effective settings.
    $entryKey = if ($Path -match '^(.*)\.all$') { $Matches[1] } else { $Path }
    $targets = @(Get-MapEntries $Map $entryKey -language 'build' -OperationContext $operationContext)
    if ($targets.Count -eq 0) {
        throw "Entry '$Path' not found."
    }

    return Invoke-WithEntrySettings -Map $Map -EntryKey $entryKey -Entry $targets[0].Value -OperationContext $operationContext -ScriptBlock {
        Get-ConfigMapSettings
    }
}

function Get-ConfigMapSetting {
    param(
        [Parameter(Mandatory)]
        [string]$Name
    )

    $settings = Get-ConfigMapSettings
    return $settings.PSObject.Properties[$Name].Value
}

function Test-ConfigMapFeatureEnabled {
    param(
        [Parameter(Mandatory)]
        [ValidateSet('TmuxAutoWindow', 'Concurrently', 'DiscoveryCache')]
        [string]$Name
    )

    switch (Get-ConfigMapSetting -Name $Name) {
        $true { return $true }
        { $_ -in '1', 'true', 'yes', 'on' } { return $true }
        default { return $false }
    }
}

Update-ConfigMapSettings | Out-Null