#Requires -RunAsAdministrator
<#
.SYNOPSIS
Starts the Soundka development audio stack: stages the APO plus the algorithm,
registers the COM server, allows unsigned modules in audiodg, and runs the local
web control service.

.DESCRIPTION
This is the endpoint-injection development path, not a production driver
package. Nothing here is bound to a device until the web page binds one, so the
machine keeps its normal audio until you press the bind button.

Typical session:

    # elevated shell, headset not plugged in yet
    .\scripts\run_dev_stack.ps1

    # plug in the USB headset, pick it in the web page, press "绑定算法到该端点",
    # start playing audio, then use bypass / strength.

    # another elevated shell, when finished
    .\scripts\run_dev_stack.ps1 -Stop

.NOTES
Requires DisableProtectedAudioDG=1, which the -Stop mode restores. The endpoint
mode-effect chain is edited in place and every endpoint touched is recorded, so
-Stop removes the CLSID again from all of them.
#>
[CmdletBinding(DefaultParameterSetName = 'Start')]
param(
    [Parameter(ParameterSetName = 'Stop')]
    [switch]$Stop,

    [Parameter(ParameterSetName = 'Status')]
    [switch]$Status,

    # Empty means "detect": a build tree uses build-apo, the portable package
    # ships the same binaries in bin\.
    [Parameter(ParameterSetName = 'Start')]
    [string]$BuildDirectory = '',

    [Parameter(ParameterSetName = 'Start')]
    [ValidateRange(1, 65535)]
    [int]$Port = 17831,

    [Parameter(ParameterSetName = 'Start')]
    [string]$Token,

    # Where the control service fetches licences from.
    #
    # Defaults to production because this script is what the shipped package
    # launches, and a customer who cannot sign in has no product. It used to
    # default to empty, which meant every account route answered "未配置账号
    # 服务，请用 --account-service 指定" -- an instruction aimed at a developer,
    # shown to a customer who has no command line to type it on.
    #
    # Pass -AccountService '' to run without one, or a local URL to develop
    # against a local service.
    [Parameter(ParameterSetName = 'Start')]
    [string]$AccountService = 'https://soundka.52game.club',

    [Parameter(ParameterSetName = 'Start')]
    [switch]$NoServer
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$stateDirectory = Join-Path $env:ProgramData 'Soundka'
$stagingDirectory = Join-Path $stateDirectory 'live-test'
$statePath = Join-Path $stateDirectory 'dev-stack-state.json'
$renderClsid = '{631D9145-CACF-44D7-861B-5D82ADD8C84F}'
$clsidKeyPath = "SOFTWARE\Classes\CLSID\$renderClsid"
# Where the audio engine looks up what an APO *is*. A CLSID with only an
# InprocServer32 can be CoCreateInstance'd by anyone, but the engine will not
# treat it as a system effect and never even attempts to load it -- silently,
# with the endpoint properties, the mode lists and the module all correct. This
# is the registration the old PnP package in packaging/dev did through its INF,
# which is why a development machine that once installed that package kept
# working while a clean machine never did.
$apoRegistrationKeyPath =
    "SOFTWARE\Classes\AudioEngine\AudioProcessingObjects\$renderClsid"
# Values mirror packaging/dev/soundka_dev_apo.inf. Flags 0x0e is
# APO_FLAG_FRAMES_PER_SECOND_MUST_MATCH | _SAMPLES_PER_FRAME_MUST_MATCH |
# _BITSPERSAMPLE_MUST_MATCH, i.e. APO_FLAG_DEFAULT.
$apoRegistration = [ordered]@{
    FriendlyName         = @('Soundka Footstep Enhancement Endpoint Effect', 'String')
    Copyright            = @('Copyright (c) Soundka', 'String')
    MajorVersion         = @(1, 'DWord')
    MinorVersion         = @(0, 'DWord')
    Flags                = @(0x0e, 'DWord')
    MinInputConnections  = @(1, 'DWord')
    MaxInputConnections  = @(1, 'DWord')
    MinOutputConnections = @(1, 'DWord')
    MaxOutputConnections = @(1, 'DWord')
    MaxInstances         = @(-1, 'DWord')
    NumAPOInterfaces     = @(1, 'DWord')
    APOInterface0        = @('{FD7F2B29-24D0-4B5C-B177-592C39F9CA10}', 'String')
}
$audioPolicyKeyPath = 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Audio'
$renderEndpointRoot =
    'SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render'
$modeEffectProperty = '{d04e05a6-594b-4fb6-a80d-01af5eed7d1d},14'
# Written by the control service only for endpoints that declared no processing
# modes of their own; see CONTROL_API.md. Removed here together with the CLSID.
$modeStreamingProperties = @(
    '{d3993a3f-99c2-4402-b5ec-a92a0367664b},6',
    '{d3993a3f-99c2-4402-b5ec-a92a0367664b},12'
)
$bindingStateRoot = 'SOFTWARE\Soundka\DevBinding'

function Get-LocalMachineKey {
    return [Microsoft.Win32.RegistryKey]::OpenBaseKey(
        [Microsoft.Win32.RegistryHive]::LocalMachine,
        [Microsoft.Win32.RegistryView]::Registry64)
}

# The MMDevices endpoint keys grant Administrators QueryValue and SetValue but
# not CreateSubKey. Asking for KEY_WRITE (which includes CreateSubKey) fails at
# open time and looks exactly like "the whole key is protected".
function Open-EndpointFxKey {
    param(
        [Parameter(Mandatory)] [string]$EndpointGuid,
        [switch]$Writable
    )

    $base = Get-LocalMachineKey
    $path = "$renderEndpointRoot\$EndpointGuid\FxProperties"
    if (-not $Writable) {
        return $base.OpenSubKey($path, $false)
    }
    $rights = [System.Security.AccessControl.RegistryRights]::QueryValues -bor
        [System.Security.AccessControl.RegistryRights]::SetValue
    return $base.OpenSubKey(
        $path,
        [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,
        $rights)
}

function Get-BoundEndpointGuid {
    $base = Get-LocalMachineKey
    $root = $base.OpenSubKey($renderEndpointRoot, $false)
    if ($null -eq $root) { return @() }
    $bound = @()
    foreach ($name in $root.GetSubKeyNames()) {
        $key = Open-EndpointFxKey -EndpointGuid $name
        if ($null -eq $key) { continue }
        try {
            $chain = @($key.GetValue($modeEffectProperty, @()))
            if ($chain -contains $renderClsid) { $bound += $name }
        } finally {
            $key.Dispose()
        }
    }
    return @($bound)
}

function Remove-EndpointBinding {
    param([Parameter(Mandatory)] [string]$EndpointGuid)

    $key = Open-EndpointFxKey -EndpointGuid $EndpointGuid -Writable
    if ($null -eq $key) {
        Write-Warning "Endpoint $EndpointGuid could not be opened for writing."
        return
    }
    try {
        $chain = @($key.GetValue($modeEffectProperty, @()))
        $remaining = @($chain | Where-Object { $_ -ne $renderClsid })
        if ($remaining.Count -eq 0) {
            $key.DeleteValue($modeEffectProperty, $false)
        } else {
            $key.SetValue(
                $modeEffectProperty,
                [string[]]$remaining,
                [Microsoft.Win32.RegistryValueKind]::MultiString)
        }
        $base = Get-LocalMachineKey
        $markerPath = "$bindingStateRoot\$EndpointGuid"
        $marker = $base.OpenSubKey($markerPath, $false)
        if ($null -ne $marker) {
            try { $addedModeList = ($marker.GetValue('AddedModeList', 0) -eq 1) }
            finally { $marker.Dispose() }
            if ($addedModeList) {
                foreach ($property in $modeStreamingProperties) {
                    $key.DeleteValue($property, $false)
                }
                Write-Host "  removed the added mode list from $EndpointGuid"
            }
            $stateRoot = $base.OpenSubKey($bindingStateRoot, $true)
            if ($null -ne $stateRoot) {
                try { $stateRoot.DeleteSubKeyTree($EndpointGuid, $false) }
                finally { $stateRoot.Dispose() }
            }
        }
        Write-Host "  unbound endpoint $EndpointGuid"
    } finally {
        $key.Dispose()
    }
}

# Endpoint properties keyed by our own CLSID, e.g. {631d9145-...},100 holding a
# SWD\DRIVERENUM\...#SOUNDKAAPO device path. Those come from the PnP package in
# packaging/dev, which no longer exists on any machine and whose software device
# is long gone. They are ours, so removing them is always correct, and a stale
# reference to a device that is not there is a plausible reason for the engine
# to give up on the CLSID without saying anything.
function Remove-EndpointResidue {
    $base = Get-LocalMachineKey
    $root = $base.OpenSubKey($renderEndpointRoot, $false)
    if ($null -eq $root) { return }
    $prefix = "$renderClsid,"
    foreach ($name in $root.GetSubKeyNames()) {
        $key = Open-EndpointFxKey -EndpointGuid $name -Writable
        if ($null -eq $key) { continue }
        try {
            $stale = @($key.GetValueNames() |
                Where-Object { $_.StartsWith($prefix, 'OrdinalIgnoreCase') })
            foreach ($value in $stale) {
                $key.DeleteValue($value, $false)
                Write-Host "  removed stale property $value from $name"
            }
        } catch {
            # An endpoint that refuses the read is not one we put anything on.
        } finally {
            $key.Dispose()
        }
    }
}

function Stop-AudioStack {
    # audiodg.exe holds the staged APO open for as long as a stream exists, so
    # restaging over a live session fails with a sharing violation. Stopping
    # Audiosrv first tears audiodg down and releases the file.
    Stop-Service -Name Audiosrv -Force -ErrorAction SilentlyContinue
    Start-Sleep -Milliseconds 500
}

function Start-AudioStack {
    Start-Service -Name Audiosrv
    Start-Sleep -Seconds 2
}

function Restart-AudioStack {
    Write-Host 'Restarting the Windows audio stack...'
    Stop-AudioStack
    Start-AudioStack
}

function Get-ProtectedAudioValue {
    $base = Get-LocalMachineKey
    $key = $base.OpenSubKey($audioPolicyKeyPath, $false)
    if ($null -eq $key) { return $null }
    try {
        return $key.GetValue('DisableProtectedAudioDG', $null)
    } finally {
        $key.Dispose()
    }
}

# The service owns the loopback port and the Global control mapping, so a
# leftover instance makes the next start fail on bind with a bare Winsock 10048
# and no hint about why. -Stop used to leave it running because it only ever
# undid registry and staging state, which is exactly the state the user cannot
# see.
function Stop-ControlService {
    $running = @(Get-Process -Name 'soundka_control_demo' -ErrorAction SilentlyContinue)
    if ($running.Count -eq 0) {
        return $false
    }
    foreach ($process in $running) {
        Write-Host "Stopping the running control service (pid $($process.Id))"
        try {
            Stop-Process -Id $process.Id -Force -ErrorAction Stop
            $process.WaitForExit(5000) | Out-Null
        } catch {
            Write-Warning ("Could not stop pid $($process.Id): " + $_.Exception.Message)
        }
    }
    # The port is released asynchronously after the handle closes, so a start
    # that follows immediately can still see it held.
    for ($attempt = 0; $attempt -lt 20; ++$attempt) {
        $inUse = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
        if ($inUse.Count -eq 0) {
            break
        }
        Start-Sleep -Milliseconds 100
    }
    return $true
}

function Show-Status {
    $protected = Get-ProtectedAudioValue
    $base = Get-LocalMachineKey
    $clsidKey = $base.OpenSubKey("$clsidKeyPath\InprocServer32", $false)
    $server = $null
    if ($null -ne $clsidKey) {
        try { $server = [string]$clsidKey.GetValue($null) } finally { $clsidKey.Dispose() }
    }
    Write-Host 'Soundka development stack status'
    Write-Host "  DisableProtectedAudioDG : $(if ($null -eq $protected) { '(absent)' } else { $protected })"
    Write-Host "  APO CLSID server        : $(if ($null -eq $server) { '(not registered)' } else { $server })"
    foreach ($file in @('soundka_windows_apo.dll', 'fse.dll', 'model_weights.enc')) {
        $path = Join-Path $stagingDirectory $file
        Write-Host "  staged $file : $(Test-Path -LiteralPath $path)"
    }
    $bound = @(Get-BoundEndpointGuid)
    if ($bound.Count -eq 0) {
        Write-Host '  bound endpoints         : (none)'
    } else {
        foreach ($guid in $bound) { Write-Host "  bound endpoint          : $guid" }
    }
    Write-Host "  recorded state          : $(Test-Path -LiteralPath $statePath)"
    $running = @(Get-Process -Name 'soundka_control_demo' -ErrorAction SilentlyContinue)
    if ($running.Count -eq 0) {
        Write-Host '  control service         : (not running)'
    } else {
        foreach ($process in $running) {
            Write-Host "  control service         : running, pid $($process.Id)"
        }
    }
}

if ($Status) {
    Show-Status
    return
}

if ($Stop) {
    Write-Host 'Removing the Soundka development stack.'
    # First, because it holds the staged DLLs open and owns the control mapping.
    (Stop-ControlService) | Out-Null
    foreach ($guid in @(Get-BoundEndpointGuid)) {
        Remove-EndpointBinding -EndpointGuid $guid
    }
    Remove-EndpointResidue

    $base = Get-LocalMachineKey
    $classes = $base.OpenSubKey('SOFTWARE\Classes\CLSID', $true)
    if ($null -ne $classes) {
        try {
            if ($classes.GetSubKeyNames() -contains $renderClsid) {
                $classes.DeleteSubKeyTree($renderClsid)
                Write-Host "  unregistered $renderClsid"
            }
        } finally {
            $classes.Dispose()
        }
    }

    $apoObjects = $base.OpenSubKey(
        'SOFTWARE\Classes\AudioEngine\AudioProcessingObjects', $true)
    if ($null -ne $apoObjects) {
        try {
            if ($apoObjects.GetSubKeyNames() -contains $renderClsid) {
                $apoObjects.DeleteSubKeyTree($renderClsid)
                Write-Host '  removed the audio engine APO registration'
            }
        } finally {
            $apoObjects.Dispose()
        }
    }

    if (Test-Path -LiteralPath $statePath) {
        $state = Get-Content -LiteralPath $statePath -Raw -Encoding UTF8 | ConvertFrom-Json
        $audioKey = $base.OpenSubKey($audioPolicyKeyPath, $true)
        if ($null -ne $audioKey) {
            try {
                if ($state.protected_audio_existed) {
                    $audioKey.SetValue(
                        'DisableProtectedAudioDG',
                        [int]$state.protected_audio_value,
                        [Microsoft.Win32.RegistryValueKind]::DWord)
                    Write-Host "  restored DisableProtectedAudioDG = $($state.protected_audio_value)"
                } else {
                    $audioKey.DeleteValue('DisableProtectedAudioDG', $false)
                    Write-Host '  removed DisableProtectedAudioDG'
                }
            } finally {
                $audioKey.Dispose()
            }
        }
        Remove-Item -LiteralPath $statePath -Force
    } else {
        Write-Warning 'No recorded state was found; DisableProtectedAudioDG was left unchanged.'
    }

    if (Test-Path -LiteralPath $stagingDirectory) {
        Remove-Item -LiteralPath $stagingDirectory -Recurse -Force -ErrorAction SilentlyContinue
    }
    Restart-AudioStack
    Write-Host 'Soundka development stack removed.' -ForegroundColor Green
    return
}

# The portable package is this same tree with the binaries in bin\ and no build
# directory, so one script drives both layouts rather than a second copy of the
# registration and rollback logic drifting out of step with this one.
if ([string]::IsNullOrWhiteSpace($BuildDirectory)) {
    # Decided by the folder, not by one binary inside it. Testing for a single
    # file meant that if anything removed it -- antivirus quarantining an
    # unsigned executable is the common one -- detection silently fell through
    # to 'build-apo' and the failure was then reported against a directory the
    # customer has never had, ending in "build the project first", which is
    # advice for a developer and useless to them.
    $BuildDirectory = if (Test-Path -LiteralPath (Join-Path $repositoryRoot 'bin') -PathType Container) {
        'bin'
    } else {
        'build-apo'
    }
}
$resolvedBuildDirectory = if ([System.IO.Path]::IsPathRooted($BuildDirectory)) {
    [System.IO.Path]::GetFullPath($BuildDirectory)
} else {
    [System.IO.Path]::GetFullPath((Join-Path $repositoryRoot $BuildDirectory))
}
# The weights ship encrypted -- see docs/algorithm_protection.md. The name
# changed from .bin to .enc, and this list is one of four places that spell
# out the same runtime files; missing one of them is how the launcher came to
# refuse a perfectly good package for lacking a file that no longer exists.
$runtimeFiles = @(
    'soundka_windows_apo.dll',
    'fse.dll',
    'model_weights.enc'
)
# A shipped package and a build tree fail for different reasons and the person
# reading the message is a different person, so they get different messages.
$isPortable = (Split-Path -Leaf $resolvedBuildDirectory) -eq 'bin'
$controlServer = Join-Path $resolvedBuildDirectory 'soundka_control_demo.exe'
$requiredFiles = @($runtimeFiles)
if (-not $NoServer) {
    # -NoServer stages and registers without running the web service, so the
    # service binary is only required when it is actually going to be started.
    $requiredFiles += 'soundka_control_demo.exe'
}
$missingFiles = @()
foreach ($file in $requiredFiles) {
    if (-not (Test-Path -LiteralPath (Join-Path $resolvedBuildDirectory $file) -PathType Leaf)) {
        $missingFiles += $file
    }
}
if ($missingFiles.Count -gt 0) {
    if ($isPortable) {
        # Every file here is unsigned, which is exactly what a scanner removes
        # on sight, so that possibility is named rather than left to guesswork.
        throw ("安装包不完整，缺少以下文件：`n  bin\" +
            ($missingFiles -join "`n  bin\") + "`n`n" +
            "请重新完整解压压缩包（不要只解压部分文件），" +
            "并检查杀毒软件是否把它们隔离了 —— 本程序未做数字签名，容易被误删。`n" +
            "文件夹位置：$resolvedBuildDirectory")
    }
    throw ("Required runtime files are missing from ${resolvedBuildDirectory}: " +
        ($missingFiles -join ', ') + " (build the project first)")
}

# A previous run's service would otherwise hold both the port and the older
# control mapping, and the failure surfaces only at the very end as a Winsock
# error, after the stack has already been reinstalled.
if (Stop-ControlService) {
    Write-Host 'Replaced a control service left over from an earlier run.'
}

New-Item -ItemType Directory -Force -Path $stagingDirectory | Out-Null
Stop-AudioStack
foreach ($file in $runtimeFiles) {
    $staged = Join-Path $stagingDirectory $file
    Copy-Item -LiteralPath (Join-Path $resolvedBuildDirectory $file) `
        -Destination $staged -Force
    # A package that arrived as a zip carries mark-of-the-web on every extracted
    # file, and the copy preserves it. It is invisible in Explorer and interacts
    # badly with SmartScreen and endpoint protection, so it is cleared here
    # rather than left as one more thing that only shows up on a customer's
    # machine.
    Unblock-File -LiteralPath $staged -ErrorAction SilentlyContinue
}
# Verbose from here on: paths, CLSIDs and registry values are ours to care
# about, not the customer's, and a folder path on screen invites poking at it.
# Warnings and errors stay visible -- they are the only lines that ask for
# action. Run with -Verbose when diagnosing a deployment.
Write-Verbose "Staged the APO and the algorithm in $stagingDirectory"

# The APO records its own load lifecycle here. audiodg runs as LOCAL SERVICE and
# is write-restricted, so the access check runs a second time against the
# restricted SID list: granting LOCAL SERVICE alone is not enough, which is the
# same lesson the shared mapping's DACL already taught. The file is created here
# with the right ACL so the module only ever has to open it for append.
$tracePath = Join-Path $stateDirectory 'apo-trace.log'
# Appended to, never truncated. Truncating on every launch destroyed exactly the
# evidence the file exists for: a module already loaded into audiodg does not run
# DllMain again, so once its lines were wiped there was no way to get them back
# short of a reboot, and the empty file was then read as proof the module had
# never loaded.
if (-not (Test-Path -LiteralPath $tracePath)) {
    Set-Content -LiteralPath $tracePath -Value '' -Encoding UTF8 -NoNewline
}
Add-Content -LiteralPath $tracePath -Encoding UTF8 -Value (
    "---- launcher started {0} ----" -f (Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))
$traceAcl = Get-Acl -LiteralPath $tracePath
# S-1-5-33 is WRITE RESTRICTED, the SID the access check runs against a second
# time for a write-restricted process. Granting LOCAL SERVICE alone leaves
# audiodg unable to write, and the trace then looks exactly like "the module was
# never loaded" -- the same mistake already made once on the shared mapping's
# DACL, repeated here on a file.
foreach ($sid in @('S-1-1-0', 'S-1-5-19', 'S-1-5-33')) {
    try {
        $traceAcl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
            (New-Object System.Security.Principal.SecurityIdentifier($sid)),
            'Modify', 'Allow')))
    } catch {
        # Not every SID resolves on every SKU; the ones that do are enough.
    }
}
Set-Acl -LiteralPath $tracePath -AclObject $traceAcl
# A DACL is not enough. audiodg runs below medium integrity, and the mandatory
# policy blocks a write to a medium-integrity object before the DACL is even
# consulted -- which is why the shared mapping's descriptor carries
# S:(ML;;NW;;;LW) and why this file, created by an elevated process and left at
# the default label, was never writable from audiodg at all.
& icacls $tracePath /setintegritylevel L | Out-Null
$traceLabel = (& icacls $tracePath | Select-String 'Mandatory Label')
Write-Verbose ("APO load trace: {0}" -f $tracePath)

# Where the debug capture writes its WAV files. Kept next to the package rather
# than in ProgramData because that is where someone looking for them will look.
#
# The directory needs the same treatment the trace file needed, and for the same
# reason: audiodg runs below medium integrity and is write-restricted, so
# creating a file here fails on the mandatory check before any ACL is consulted.
# Labelling one file was not enough -- a new file inherits the directory's
# label, and the directory had none, which is why exporting reported failure.
$captureDirectory = Join-Path $repositoryRoot 'wav'
New-Item -ItemType Directory -Force -Path $captureDirectory | Out-Null
$captureAcl = Get-Acl -LiteralPath $captureDirectory
foreach ($sid in @('S-1-1-0', 'S-1-5-19', 'S-1-5-33')) {
    try {
        $captureAcl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
            (New-Object System.Security.Principal.SecurityIdentifier($sid)),
            'Modify',
            'ContainerInherit,ObjectInherit',
            'None',
            'Allow')))
    } catch {
        # Not every SID resolves on every SKU; the ones that do are enough.
    }
}
Set-Acl -LiteralPath $captureDirectory -AclObject $captureAcl
# (OI)(CI) so files created later inherit the label instead of defaulting to
# medium and being unwritable all over again.
& icacls $captureDirectory /setintegritylevel "(OI)(CI)L" | Out-Null
$captureLabel = (& icacls $captureDirectory | Select-String 'Mandatory Label')
Write-Verbose ("Capture folder: {0}" -f $captureDirectory)
if ($captureLabel) {
    Write-Verbose ("  integrity: {0}" -f ($captureLabel -join ' ').Trim())
} else {
    Write-Warning 'The capture folder has no low integrity label; exporting will fail.'
}

# Handed to the APO through the registry: audiodg cannot see this script's
# variables, and the shared mapping carries counters rather than paths.
$captureKey = (Get-LocalMachineKey).CreateSubKey('SOFTWARE\Soundka')
try {
    $captureKey.SetValue(
        'CaptureDirectory', $captureDirectory, [Microsoft.Win32.RegistryValueKind]::String)
} finally {
    $captureKey.Dispose()
}
if ($traceLabel) {
    Write-Verbose ("  integrity: {0}" -f ($traceLabel -join ' ').Trim())
} else {
    Write-Warning 'The trace file has no low integrity label; audiodg will not be able to write to it.'
}

# A license dropped next to the package is installed once, into the shared state
# directory the APO reads from inside audiodg. An existing installed license is
# never overwritten, so a license loaded through the web page survives restarts.
$licenseSource = Join-Path $repositoryRoot 'soundka.license'
$licenseTarget = Join-Path $stateDirectory 'soundka.license'
if ((Test-Path -LiteralPath $licenseSource -PathType Leaf) -and
    -not (Test-Path -LiteralPath $licenseTarget -PathType Leaf)) {
    New-Item -ItemType Directory -Force -Path $stateDirectory | Out-Null
    Copy-Item -LiteralPath $licenseSource -Destination $licenseTarget -Force
    Write-Host "Installed the license from $licenseSource"
}

if (-not (Test-Path -LiteralPath $statePath)) {
    $protectedBefore = Get-ProtectedAudioValue
    $state = [ordered]@{
        created = [DateTime]::Now.ToString('o')
        protected_audio_existed = ($null -ne $protectedBefore)
        protected_audio_value = $protectedBefore
        staging_directory = $stagingDirectory
    }
    New-Item -ItemType Directory -Force -Path $stateDirectory | Out-Null
    $state | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $statePath -Encoding UTF8
    Write-Host "Recorded rollback state in $statePath"
}

$base = Get-LocalMachineKey
$clsidKey = $base.CreateSubKey("$clsidKeyPath\InprocServer32")
try {
    $clsidKey.SetValue(
        $null,
        (Join-Path $stagingDirectory 'soundka_windows_apo.dll'),
        [Microsoft.Win32.RegistryValueKind]::String)
    $clsidKey.SetValue(
        'ThreadingModel', 'Both', [Microsoft.Win32.RegistryValueKind]::String)
} finally {
    $clsidKey.Dispose()
}
Write-Verbose "Registered $renderClsid"

$apoKey = $base.CreateSubKey($apoRegistrationKeyPath)
try {
    foreach ($name in $apoRegistration.Keys) {
        $value = $apoRegistration[$name][0]
        $kind = $apoRegistration[$name][1]
        $apoKey.SetValue($name, $value, [Microsoft.Win32.RegistryValueKind]::$kind)
    }
} finally {
    $apoKey.Dispose()
}
Write-Verbose 'Registered the APO with the audio engine'

$audioKey = $base.CreateSubKey($audioPolicyKeyPath)
try {
    $audioKey.SetValue(
        'DisableProtectedAudioDG', 1, [Microsoft.Win32.RegistryValueKind]::DWord)
} finally {
    $audioKey.Dispose()
}
Write-Verbose 'Set DisableProtectedAudioDG = 1 (audiodg will load unsigned modules)'

Restart-AudioStack

if ($NoServer) {
    Show-Status
    return
}

if ([string]::IsNullOrWhiteSpace($Token)) {
    $bytes = [byte[]]::new(24)
    [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
    $Token = -join ($bytes | ForEach-Object { $_.ToString('x2') })
}

# The service prints the address and opens the browser itself, so the only
# thing worth adding here is how to stop. Everything else the user needs is on
# the page.
Write-Host ''
Write-Host ("Stop with:  .\scripts\run_dev_stack.ps1 -Stop  (elevated)")
Write-Host ''

# The service must run elevated and in this session: it creates the Global
# control mapping the APO in audiodg attaches to, and it writes the endpoint
# effect chain when the web page binds a device.
#
# --open-browser is passed here rather than defaulted in the service, because
# the tests start the service too and must not have windows opening on them.
$serverArguments = @(
    '--port', $Port,
    '--web-root', (Join-Path $repositoryRoot 'web'),
    '--token', $Token,
    '--control-map', 'Global\SoundkaControlV1',
    '--open-browser')
if ($AccountService) {
    $serverArguments += @('--account-service', $AccountService)
}
if ($VerbosePreference -ne 'SilentlyContinue') {
    $serverArguments += '--verbose'
}
& $controlServer @serverArguments
