param(
    [Parameter(Mandatory = $true)]
    [string]$ExecutablePath,
    [string]$ProjectToken = 'UnrealHyperTwist',
    [string[]]$AdditionalArguments = @(
        '-notraceserver',
        '-traceautostart=0'
    ),
    [string]$ReportPath = '',
    [switch]$VerifyOnly
)

$ErrorActionPreference = 'Stop'

$RawDataResourceType = 10
$BootstrapArgumentsResourceId = 202

function Write-Utf8JsonFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [object]$Value
    )

    $ParentPath = Split-Path -Parent $Path
    if (-not [string]::IsNullOrWhiteSpace($ParentPath))
    {
        New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
    }

    [System.IO.File]::WriteAllText(
        $Path,
        ($Value | ConvertTo-Json -Depth 10),
        (New-Object System.Text.UTF8Encoding($false))
    )
}

if (-not ('HyperTwistBootstrapResources.NativeMethods' -as [type]))
{
    Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;

namespace HyperTwistBootstrapResources
{
    public static class NativeMethods
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern IntPtr LoadLibraryEx(
            string fileName,
            IntPtr fileHandle,
            uint flags
        );

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool FreeLibrary(IntPtr moduleHandle);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr FindResource(
            IntPtr moduleHandle,
            IntPtr resourceName,
            IntPtr resourceType
        );

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern uint SizeofResource(
            IntPtr moduleHandle,
            IntPtr resourceInfo
        );

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr LoadResource(
            IntPtr moduleHandle,
            IntPtr resourceInfo
        );

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr LockResource(IntPtr resourceData);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern IntPtr BeginUpdateResource(
            string fileName,
            bool deleteExistingResources
        );

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool UpdateResource(
            IntPtr updateHandle,
            IntPtr resourceType,
            IntPtr resourceName,
            ushort language,
            byte[] data,
            uint dataLength
        );

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool EndUpdateResource(
            IntPtr updateHandle,
            bool discard
        );
    }
}
'@
}

function New-Win32Exception {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Operation
    )

    $ErrorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
    return New-Object `
        -TypeName ComponentModel.Win32Exception `
        -ArgumentList $ErrorCode, "$Operation failed with Win32 error $ErrorCode."
}

function Get-BootstrapArgumentsResource {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $LoadLibraryAsDataFile = 0x00000002
    $LoadLibraryAsImageResource = 0x00000020
    $ModuleHandle = [HyperTwistBootstrapResources.NativeMethods]::LoadLibraryEx(
        $Path,
        [IntPtr]::Zero,
        ($LoadLibraryAsDataFile -bor $LoadLibraryAsImageResource)
    )
    if ($ModuleHandle -eq [IntPtr]::Zero)
    {
        throw (New-Win32Exception -Operation "LoadLibraryEx('$Path')")
    }

    try
    {
        $ResourceInfo = [HyperTwistBootstrapResources.NativeMethods]::FindResource(
            $ModuleHandle,
            [IntPtr]$BootstrapArgumentsResourceId,
            [IntPtr]$RawDataResourceType
        )
        if ($ResourceInfo -eq [IntPtr]::Zero)
        {
            throw (
                New-Win32Exception -Operation (
                    "FindResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
                )
            )
        }

        $ResourceSize = [HyperTwistBootstrapResources.NativeMethods]::SizeofResource(
            $ModuleHandle,
            $ResourceInfo
        )
        if ($ResourceSize -eq 0)
        {
            throw (
                New-Win32Exception -Operation (
                    "SizeofResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
                )
            )
        }

        $LoadedResource = [HyperTwistBootstrapResources.NativeMethods]::LoadResource(
            $ModuleHandle,
            $ResourceInfo
        )
        if ($LoadedResource -eq [IntPtr]::Zero)
        {
            throw (
                New-Win32Exception -Operation (
                    "LoadResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
                )
            )
        }

        $ResourcePointer = [HyperTwistBootstrapResources.NativeMethods]::LockResource(
            $LoadedResource
        )
        if ($ResourcePointer -eq [IntPtr]::Zero)
        {
            throw (
                New-Win32Exception -Operation (
                    "LockResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
                )
            )
        }

        $ResourceBytes = New-Object byte[] $ResourceSize
        [Runtime.InteropServices.Marshal]::Copy(
            $ResourcePointer,
            $ResourceBytes,
            0,
            [int]$ResourceSize
        )
        return [Text.Encoding]::Unicode.GetString($ResourceBytes).TrimEnd([char]0)
    }
    finally
    {
        [void][HyperTwistBootstrapResources.NativeMethods]::FreeLibrary($ModuleHandle)
    }
}

function Set-BootstrapArgumentsResource {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [string]$Value
    )

    $ResourceBytes = [Text.Encoding]::Unicode.GetBytes($Value + [char]0)
    $UpdateHandle = [HyperTwistBootstrapResources.NativeMethods]::BeginUpdateResource(
        $Path,
        $false
    )
    if ($UpdateHandle -eq [IntPtr]::Zero)
    {
        throw (New-Win32Exception -Operation "BeginUpdateResource('$Path')")
    }

    $UpdateClosed = $false
    try
    {
        if (-not [HyperTwistBootstrapResources.NativeMethods]::UpdateResource(
            $UpdateHandle,
            [IntPtr]$RawDataResourceType,
            [IntPtr]$BootstrapArgumentsResourceId,
            [uint16]0,
            $ResourceBytes,
            [uint32]$ResourceBytes.Length
        ))
        {
            throw (
                New-Win32Exception -Operation (
                    "UpdateResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)"
                )
            )
        }

        $UpdateCompleted = [HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource(
            $UpdateHandle,
            $false
        )
        $UpdateClosed = $true
        if (-not $UpdateCompleted)
        {
            throw (New-Win32Exception -Operation "EndUpdateResource('$Path')")
        }
    }
    finally
    {
        if (-not $UpdateClosed)
        {
            [void][HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource(
                $UpdateHandle,
                $true
            )
        }
    }
}

if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf))
{
    throw "HyperTwist bootstrap executable '$ExecutablePath' was not found."
}

if ([string]::IsNullOrWhiteSpace($ProjectToken))
{
    throw 'ProjectToken must not be empty.'
}

$NormalizedAdditionalArguments = @(
    $AdditionalArguments |
        ForEach-Object {
            if ($null -eq $_)
            {
                return
            }

            $_.Trim()
        } |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
foreach ($Argument in $NormalizedAdditionalArguments)
{
    if ($Argument.IndexOf([char]0) -ge 0 `
        -or $Argument.IndexOf("`r") -ge 0 `
        -or $Argument.IndexOf("`n") -ge 0)
    {
        throw "Bootstrap launch argument '$Argument' contains an unsupported control character."
    }
}

$ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path
$ExpectedArguments = @($ProjectToken) + $NormalizedAdditionalArguments
$ExpectedResourceValue = $ExpectedArguments -join ' '
if ([string]::IsNullOrWhiteSpace($ReportPath))
{
    $ReportPath = "$ResolvedExecutablePath.bootstrap-launch-arguments.json"
}

$OriginalHash = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant()
$OriginalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath
$Result = [ordered]@{
    reportVersion = 'ht-bootstrap-launch-arguments/v1'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    executablePath = $ResolvedExecutablePath
    resourceType = $RawDataResourceType
    resourceId = $BootstrapArgumentsResourceId
    verifyOnly = [bool]$VerifyOnly
    expectedArguments = @($ExpectedArguments)
    expectedResourceValue = $ExpectedResourceValue
    originalResourceValue = $OriginalResourceValue
    finalResourceValue = $null
    originalSha256 = $OriginalHash
    finalSha256 = $null
    changed = $false
    result = 'failed'
    error = $null
}

$TemporaryPath = $null
$BackupPath = $null
try
{
    if ($VerifyOnly)
    {
        if ($OriginalResourceValue -ne $ExpectedResourceValue)
        {
            throw (
                "Bootstrap argument resource was '$OriginalResourceValue'; " +
                "expected '$ExpectedResourceValue'."
            )
        }
    }
    elseif ($OriginalResourceValue -ne $ExpectedResourceValue)
    {
        $TemporaryPath = '{0}.bootstrap-{1}-{2}.tmp' -f (
            $ResolvedExecutablePath,
            $PID,
            [Guid]::NewGuid().ToString('N')
        )
        $BackupPath = '{0}.bootstrap-{1}-{2}.bak' -f (
            $ResolvedExecutablePath,
            $PID,
            [Guid]::NewGuid().ToString('N')
        )
        Copy-Item -LiteralPath $ResolvedExecutablePath -Destination $TemporaryPath -Force
        Set-BootstrapArgumentsResource -Path $TemporaryPath -Value $ExpectedResourceValue

        $TemporaryResourceValue = Get-BootstrapArgumentsResource -Path $TemporaryPath
        if ($TemporaryResourceValue -ne $ExpectedResourceValue)
        {
            throw (
                "Patched bootstrap argument resource was '$TemporaryResourceValue'; " +
                "expected '$ExpectedResourceValue'."
            )
        }

        [System.IO.File]::Replace(
            $TemporaryPath,
            $ResolvedExecutablePath,
            $BackupPath,
            $true
        )
        $TemporaryPath = $null
        $Result.changed = $true
    }

    $Result.finalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath
    if ($Result.finalResourceValue -ne $ExpectedResourceValue)
    {
        throw (
            "Final bootstrap argument resource was '$($Result.finalResourceValue)'; " +
            "expected '$ExpectedResourceValue'."
        )
    }

    $Result.finalSha256 = (
        Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256
    ).Hash.ToLowerInvariant()
    $Result.result = 'passed'
}
catch
{
    $Result.error = $_.Exception.Message
    throw
}
finally
{
    if (-not [string]::IsNullOrWhiteSpace($TemporaryPath) `
        -and (Test-Path -LiteralPath $TemporaryPath))
    {
        Remove-Item -LiteralPath $TemporaryPath -Force -ErrorAction SilentlyContinue
    }
    if (-not [string]::IsNullOrWhiteSpace($BackupPath) `
        -and (Test-Path -LiteralPath $BackupPath))
    {
        Remove-Item -LiteralPath $BackupPath -Force -ErrorAction SilentlyContinue
    }

    Write-Utf8JsonFile -Path $ReportPath -Value $Result
}

$Result | ConvertTo-Json -Depth 10
