# Requires -Version 5.1
[CmdletBinding()]
param(
    [string]$PackRoot = (Join-Path $PSScriptRoot 'MinecraftAccessibilityPack'),
    [string]$TargetMinecraftDir = (Join-Path $env:APPDATA '.minecraft'),
    [string]$MinecraftVersion = '1.21.10',
    [string]$FabricLoaderVersion = '0.17.3',
    [string]$ProfileName = 'Visually Impaired Access',
    [ValidateSet('auto', 'microsoft_store', 'win32')]
    [string]$LauncherType = 'auto',
    [switch]$SkipLauncherInstall,
    [switch]$SkipJavaInstall
)

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

$managedDirectories = @(
    'mods',
    'resourcepacks',
    'config',
    'data',
    'defaultconfigs',
    'espeak-ng-data'
)

$managedFiles = @(
    'audio_navigation_tts_0.3.1_x86_64.dll',
    'nvdaControllerClient64.dll',
    'options.txt',
    'SAAPI64.dll',
    'Tolk.dll',
    'treatment_tags.json'
)

$compatibilityFallbacks = @(
    '1.21.10',
    '1.21.9',
    '1.21.8',
    '1.21.7',
    '1.21.6',
    '1.21.5',
    '1.21.4',
    '1.21.3',
    '1.21.2',
    '1.21.1',
    '1.21'
)

function Write-Step {
    param([string]$Message)
    Write-Host ''
    Write-Host "== $Message ==" -ForegroundColor Cyan
}

function Ensure-Directory {
    param([string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
}

function Install-WingetPackage {
    param(
        [string]$Id,
        [string]$Label
    )

    $winget = Get-Command winget -ErrorAction SilentlyContinue
    if (-not $winget) {
        throw "winget was not found. Install $Label manually and run this script again."
    }

    $listOutput = & $winget.Source list --id $Id --exact --source winget 2>&1
    if ($LASTEXITCODE -eq 0) {
        Write-Host "$Label is already installed."
        return
    }

    Write-Host "Installing $Label with winget..."
    & $winget.Source install --id $Id --exact --accept-package-agreements --accept-source-agreements --source winget --disable-interactivity
    if ($LASTEXITCODE -ne 0) {
        throw "winget failed while installing $Label."
    }
}

function Get-JavaPath {
    $java = Get-Command java -ErrorAction SilentlyContinue
    if ($java) {
        return $java.Source
    }

    return $null
}

function Install-JavaIfNeeded {
    if ($SkipJavaInstall) {
        return
    }

    if (Get-JavaPath) {
        return
    }

    Install-WingetPackage -Id 'EclipseAdoptium.Temurin.21.JRE' -Label 'Temurin 21 JRE'
}

function Install-LauncherIfNeeded {
    if ($SkipLauncherInstall) {
        return
    }

    Install-WingetPackage -Id 'Mojang.MinecraftLauncher' -Label 'Minecraft Launcher'
}

function Get-LatestFabricInstaller {
    $meta = Invoke-RestMethod -Uri 'https://meta.fabricmc.net/v2/versions/installer'
    $installer = $meta | Where-Object { $_.stable } | Select-Object -First 1
    if (-not $installer) {
        throw 'Could not resolve the latest stable Fabric installer.'
    }

    return $installer
}

function Get-DetectedLauncherType {
    $storePackage = Get-AppxPackage -Name 'Microsoft.4297127D64EC6' -ErrorAction SilentlyContinue
    if ($storePackage) {
        return 'microsoft_store'
    }

    $win32Candidates = @(
        'C:\Program Files (x86)\Minecraft Launcher\MinecraftLauncher.exe',
        'C:\Program Files\Minecraft Launcher\MinecraftLauncher.exe'
    )

    foreach ($candidate in $win32Candidates) {
        if (Test-Path -LiteralPath $candidate) {
            return 'win32'
        }
    }

    return $null
}

function Get-LauncherInstallOrder {
    param([string]$PreferredLauncherType)

    if ($PreferredLauncherType -ne 'auto') {
        return @($PreferredLauncherType)
    }

    $detected = Get-DetectedLauncherType
    if ($detected -eq 'microsoft_store') {
        return @('microsoft_store', 'win32')
    }

    if ($detected -eq 'win32') {
        return @('win32', 'microsoft_store')
    }

    return @('microsoft_store', 'win32')
}

function Install-FabricProfile {
    param(
        [string]$InstallDir,
        [string]$GameVersion,
        [string]$LoaderVersion,
        [string[]]$LauncherKinds
    )

    $javaPath = Get-JavaPath
    if (-not $javaPath) {
        throw 'Java was not found after the Java installation step.'
    }

    $installer = Get-LatestFabricInstaller
    $installerJar = Join-Path $env:TEMP ("fabric-installer-" + $installer.version + '.jar')

    Write-Host "Downloading Fabric installer $($installer.version)..."
    Invoke-WebRequest -UseBasicParsing -Uri $installer.url -OutFile $installerJar

    $succeeded = $false
    $attemptErrors = @()

    foreach ($launcherKind in $LauncherKinds) {
        Write-Host "Installing Fabric loader $LoaderVersion for Minecraft $GameVersion using launcher type $launcherKind..."
        & $javaPath -jar $installerJar client -dir $InstallDir -mcversion $GameVersion -loader $LoaderVersion -launcher $launcherKind
        if ($LASTEXITCODE -eq 0) {
            $succeeded = $true
            break
        }

        $attemptErrors += $launcherKind
    }

    if (-not $succeeded) {
        throw ('Fabric installer failed for launcher types: ' + ($attemptErrors -join ', '))
    }
}

function Backup-And-ClearTarget {
    param(
        [string]$TargetDir,
        [string[]]$RelativePaths
    )

    $existing = @()
    foreach ($relativePath in $RelativePaths) {
        $fullPath = Join-Path $TargetDir $relativePath
        if (Test-Path -LiteralPath $fullPath) {
            $existing += $relativePath
        }
    }

    if ($existing.Count -eq 0) {
        return $null
    }

    $backupRoot = Join-Path (Split-Path -Parent $TargetDir) ('minecraft-accessibility-backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
    Ensure-Directory -Path $backupRoot

    foreach ($relativePath in $existing) {
        $sourcePath = Join-Path $TargetDir $relativePath
        $backupPath = Join-Path $backupRoot $relativePath
        $backupParent = Split-Path -Parent $backupPath
        if ($backupParent) {
            Ensure-Directory -Path $backupParent
        }

        Copy-Item -LiteralPath $sourcePath -Destination $backupPath -Recurse -Force
        Remove-Item -LiteralPath $sourcePath -Recurse -Force
    }

    return $backupRoot
}

function Copy-PackFiles {
    param(
        [string]$SourceRoot,
        [string]$DestinationRoot
    )

    foreach ($relativePath in ($managedDirectories + $managedFiles)) {
        $sourcePath = Join-Path $SourceRoot $relativePath
        if (-not (Test-Path -LiteralPath $sourcePath)) {
            continue
        }

        $destinationPath = Join-Path $DestinationRoot $relativePath
        $destinationParent = Split-Path -Parent $destinationPath
        if ($destinationParent) {
            Ensure-Directory -Path $destinationParent
        }

        Copy-Item -LiteralPath $sourcePath -Destination $destinationPath -Recurse -Force
    }
}

function Get-Manifest {
    param([string]$ManifestPath)

    if (-not (Test-Path -LiteralPath $ManifestPath)) {
        throw "Manifest file not found: $ManifestPath"
    }

    return Get-Content -Raw -LiteralPath $ManifestPath | ConvertFrom-Json
}

function Select-CompatibleVersion {
    param(
        [object[]]$Versions,
        [string]$ProjectType,
        [string[]]$GameVersionFallbacks
    )

    foreach ($gameVersion in $GameVersionFallbacks) {
        $match = $Versions |
            Where-Object {
                $_.game_versions -contains $gameVersion -and
                ($ProjectType -ne 'mod' -or $_.loaders -contains 'fabric')
            } |
            Sort-Object { [DateTime]$_.date_published } -Descending |
            Select-Object -First 1

        if ($match) {
            return $match
        }
    }

    return $null
}

function Download-ModrinthEntry {
    param(
        [object]$Entry,
        [string]$DestinationRoot,
        [string[]]$GameVersionFallbacks
    )

    $targetSubdirectory = if ($Entry.projectType -eq 'resourcepack') { 'resourcepacks' } else { 'mods' }
    $destinationDirectory = Join-Path $DestinationRoot $targetSubdirectory
    Ensure-Directory -Path $destinationDirectory

    $versionEndpoint = if ($Entry.projectType -eq 'resourcepack') {
        'https://api.modrinth.com/v2/project/' + $Entry.projectId + '/version'
    }
    else {
        $loaders = [Uri]::EscapeDataString('["fabric"]')
        'https://api.modrinth.com/v2/project/' + $Entry.projectId + '/version?loaders=' + $loaders
    }

    $versions = Invoke-RestMethod -Uri $versionEndpoint
    $selectedVersion = Select-CompatibleVersion -Versions $versions -ProjectType $Entry.projectType -GameVersionFallbacks $GameVersionFallbacks
    if (-not $selectedVersion) {
        throw "No compatible version was found for $($Entry.name)."
    }

    $downloadFile = $selectedVersion.files | Where-Object { $_.primary } | Select-Object -First 1
    if (-not $downloadFile) {
        $downloadFile = $selectedVersion.files | Select-Object -First 1
    }

    if (-not $downloadFile) {
        throw "No downloadable file was found for $($Entry.name)."
    }

    $targetFileName = if ($Entry.PSObject.Properties.Name -contains 'targetFileName' -and $Entry.targetFileName) {
        [string]$Entry.targetFileName
    }
    else {
        [string]$downloadFile.filename
    }

    $targetPath = Join-Path $destinationDirectory $targetFileName
    Write-Host ("Downloading " + $Entry.name + " -> " + $targetFileName)
    Invoke-WebRequest -UseBasicParsing -Uri $downloadFile.url -OutFile $targetPath
}

function Set-LauncherProfile {
    param(
        [string]$TargetDir,
        [string]$ProfileDisplayName,
        [string]$VersionId
    )

    $profilePath = Join-Path $TargetDir 'launcher_profiles.json'
    $msStorePath = Join-Path $TargetDir 'launcher_profiles_microsoft_store.json'
    if (-not (Test-Path -LiteralPath $profilePath) -and (Test-Path -LiteralPath $msStorePath)) {
        $profilePath = $msStorePath
    }
    $timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')

    if (Test-Path -LiteralPath $profilePath) {
        $profilesJson = Get-Content -Raw -LiteralPath $profilePath | ConvertFrom-Json
    }
    else {
        $profilesJson = [PSCustomObject]@{
            profiles = [PSCustomObject]@{}
            settings = [PSCustomObject]@{}
            version  = 6
        }
    }

    if (-not $profilesJson.PSObject.Properties['profiles']) {
        Add-Member -InputObject $profilesJson -MemberType NoteProperty -Name profiles -Value ([PSCustomObject]@{})
    }

    $profileId = 'fabric-loader-' + $MinecraftVersion

    if (-not $profilesJson.profiles.PSObject.Properties[$profileId]) {
        Add-Member -InputObject $profilesJson.profiles -MemberType NoteProperty -Name $profileId -Value ([PSCustomObject]@{})
    }

    $profile = $profilesJson.profiles.$profileId

    if (-not $profile.PSObject.Properties['created']) {
        Add-Member -InputObject $profile -MemberType NoteProperty -Name created -Value $timestamp
    }
    else {
        $profile.created = if ($profile.created) { $profile.created } else { $timestamp }
    }

    foreach ($property in @('icon', 'lastUsed', 'lastVersionId', 'name', 'type')) {
        if (-not $profile.PSObject.Properties[$property]) {
            Add-Member -InputObject $profile -MemberType NoteProperty -Name $property -Value $null
        }
    }

    $profile.icon = 'Grass'
    $profile.lastUsed = $timestamp
    $profile.lastVersionId = $VersionId
    $profile.name = $ProfileDisplayName
    $profile.type = 'custom'

    $profilesJson | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $profilePath -Encoding UTF8
}

Write-Step -Message 'Checking install pack'

$manifestPath = Join-Path $PackRoot 'modrinth-manifest.json'
if (-not (Test-Path -LiteralPath $PackRoot)) {
    throw "Pack folder not found: $PackRoot"
}

$manifest = Get-Manifest -ManifestPath $manifestPath

Write-Step -Message 'Installing prerequisites'
Install-LauncherIfNeeded
Install-JavaIfNeeded

Write-Step -Message 'Installing Fabric profile'
Ensure-Directory -Path $TargetMinecraftDir
$launcherOrder = Get-LauncherInstallOrder -PreferredLauncherType $LauncherType
Install-FabricProfile -InstallDir $TargetMinecraftDir -GameVersion $MinecraftVersion -LoaderVersion $FabricLoaderVersion -LauncherKinds $launcherOrder

Write-Step -Message 'Backing up current managed files'
$backupPath = Backup-And-ClearTarget -TargetDir $TargetMinecraftDir -RelativePaths ($managedDirectories + $managedFiles)
if ($backupPath) {
    Write-Host "Backup created at $backupPath"
}
else {
    Write-Host 'No existing managed files were found to back up.'
}

Write-Step -Message 'Copying packaged accessibility settings and support files'
Copy-PackFiles -SourceRoot $PackRoot -DestinationRoot $TargetMinecraftDir

Write-Step -Message 'Downloading latest compatible mods and resource packs'
foreach ($entry in $manifest) {
    Download-ModrinthEntry -Entry $entry -DestinationRoot $TargetMinecraftDir -GameVersionFallbacks $compatibilityFallbacks
}

Write-Step -Message 'Updating launcher profile'
$targetVersionId = 'fabric-loader-' + $FabricLoaderVersion + '-' + $MinecraftVersion
Set-LauncherProfile -TargetDir $TargetMinecraftDir -ProfileDisplayName $ProfileName -VersionId $targetVersionId

Write-Step -Message 'Done'
Write-Host 'Installation complete.'
Write-Host 'Next steps:'
Write-Host '1. Open Minecraft Launcher.'
Write-Host ('2. Sign in with the account that owns Minecraft if needed.')
Write-Host ('3. Launch the "' + $ProfileName + '" profile.')
Write-Host '4. Confirm your screen reader is running before you enter a world.'
