<#
.SYNOPSIS
    Origin configuration & installation script

.DESCRIPTION
    Configure Origin to not update automatically and optionally install the last
    known stable version to avoid being forced to migrate to the new EA app.

.LINK
    https://github.com/alexitx/stop-origin-migration
#>

$ErrorActionPreference = "Stop"

$VERSION = "0.1.0"
$SCRIPT_DIR = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$SEPARATOR = "-" * 80

function Test-Administrator {
    $CurrentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
    return $CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}

function New-TemporaryDirectory {
    $TempDir = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ([Guid]::NewGuid())
    return New-Item -Path $TempDir -ItemType Directory
}

function Join-Files {
    param (
        [parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string[]] $Files,

        [parameter(Mandatory=$true)]
        [string] $Destination
    )

    begin {
        $OutFile = [IO.File]::Create($Destination)
    }

    process {
        try {
            foreach ($File in $Files) {
                $InFile = [IO.File]::OpenRead($File)
                try {
                    $InFile.CopyTo($OutFile)
                } finally {
                    $InFile.Dispose()
                }
            }
        } catch {
            $OutFile.Dispose()
            throw
        }
    }

    end {
        $OutFile.Dispose()
    }
}

function Invoke-DownloadFile {
    param (
        [Parameter(Mandatory=$true)]
        [uri]
        $Uri,

        [Parameter(Mandatory=$true)]
        [IO.FileInfo]
        $Destination,

        [Parameter()]
        [ValidateRange(0, [int]::MaxValue)]
        [int] $MaxRetries = 0,

        [Parameter()]
        [ValidateRange(0, [int]::MaxValue)]
        [int] $Timeout = 10000
    )

    $RetryCount = 0
    while ($true) {
        if ($RetryCount -ge 1) {
            Write-Host "Retrying download ($RetryCount / $MaxRetries)"
        }

        try {
            $Request = [System.Net.HttpWebRequest]::Create($Uri)
            $Request.Timeout = $Timeout

            $Response = $Request.GetResponse()
            $ResponseStream = $Response.GetResponseStream()
            $ContentLength = $Response.ContentLength

            $TargetStream = [IO.File]::Create($Destination.FullName)

            $BufferSize = 16 * 1024 # 16 KiB
            $Buffer = New-Object byte[] $BufferSize;

            $Count = 0
            $DownloadedBytes = 0

            $UpdateRate = 500 # 500 ms
            $LastProgress = [datetime] 0

            while ($true) {
                $Now = Get-Date
                $TimeElapsed = (New-TimeSpan -Start $LastProgress -End $Now).TotalMilliseconds
                if ($TimeElapsed -ge $UpdateRate) {
                    $PercentComplete = $DownloadedBytes / $ContentLength * 100
                    Write-Progress `
                        -Activity "Downloading '$($Destination.Name)'" `
                        -Status "Percent complete: $($PercentComplete.ToString("0.00"))%" `
                        -PercentComplete $PercentComplete
                    $LastProgress = $Now
                }

                $Count = $ResponseStream.Read($Buffer, 0, $Buffer.Length)
                if ($Count -le 0) {
                    break
                }

                $TargetStream.Write($Buffer, 0, $Count)
                $DownloadedBytes += $Count
            }

            Write-Progress -Activity "Downloading '$($Destination.Name)'" -Completed

            break
        } catch [Net.WebException] {
            if ($RetryCount -ge $MaxRetries) {
                throw
            }

            Write-Host -ForegroundColor Red "Download error: $($_.Exception.Message)"
            $RetryCount++
        } finally {
            if ($ResponseStream) {
                $ResponseStream.Dispose()
            }
            if ($TargetStream) {
                $TargetStream.Dispose()
            }
        }
    }
}

function Set-FileOwnership {
    param (
        [Parameter(Mandatory=$true)]
        [IO.FileInfo] $Path
    )

    $ACL = $Path.GetAccessControl()
    $CurrentUser = New-Object Security.Principal.NTAccount([Security.Principal.WindowsIdentity]::GetCurrent().Name)
    $ACL.SetOwner($CurrentUser)
    $ACL.SetAccessRuleProtection($false, $true) # Preserve inheritance
    $Path.SetAccessControl($ACL)
}

function Update-Settings {
    param (
        [Parameter(Mandatory=$true)]
        [xml] $TargetXml,

        [Parameter(Mandatory=$true)]
        [xml] $BaseXml
    )

    $BaseXml.SelectNodes("/Settings/Setting[@key]") | ForEach-Object {
        $BaseSetting = $_
        $TargetSetting = $TargetXml.SelectSingleNode("/Settings/Setting[@key='$($BaseSetting.GetAttribute("key"))']")
        $ImportedTargetSetting = $TargetXml.ImportNode($BaseSetting, $false)
        if ($TargetSetting) {
            # Replace existing target setting element
            [void] $TargetXml.SelectSingleNode("/Settings").ReplaceChild($ImportedTargetSetting, $TargetSetting)
        } else {
            # Add new setting element to target
            [void] $TargetXml.SelectSingleNode("/Settings").AppendChild($ImportedTargetSetting)
        }
    }
}

function Remove-Settings {
    param (
        [Parameter(Mandatory=$true)]
        [xml] $TargetXml,

        [Parameter(Mandatory=$true)]
        [string[]] $Keys
    )

    foreach ($Key in $Keys) {
        $TargetSetting = $TargetXml.SelectSingleNode("/Settings/Setting[@key='$Key']")
        if ($TargetSetting) {
            [void] $TargetXml.SelectSingleNode("/Settings").RemoveChild($TargetSetting)
        }
    }
}

function Show-Prompt {
    param (
        [Parameter(Mandatory=$true)]
        [string] $Caption,

        [Parameter(Mandatory=$true)]
        [string] $Message,

        [Parameter()]
        [string[]] $Choices = @("&Yes", "&No"),

        [Parameter()]
        [ValidateRange(0, [int]::MaxValue)]
        [int] $DefaultChoice = 0
    )

    Write-Host -ForegroundColor Green "`n$Caption"

    $Decision = $Host.UI.PromptForChoice($null, $Message, $Choices, $DefaultChoice)

    Write-Host -NoNewLine "`n"

    return $Decision
}

# Elevate if not running as administrator
if (-not (Test-Administrator)) {
    Write-Host "Elevating to administrator"

    $ScriptPath = $MyInvocation.MyCommand.Definition
    if (-not $ScriptPath) {
        Write-Host -ForegroundColor Red "You need to run this script as an administrator"
        exit 1
    }

    try {
        $ShellArguments = @("-NoLogo", "-NoExit", "-ExecutionPolicy", "Bypass", "-File", $ScriptPath)
        Start-Process powershell.exe -ArgumentList $ShellArguments -Verb RunAs
    } catch [InvalidOperationException] {
        Write-Host -ForegroundColor Red "You need to run this script as an administrator"
        Read-Host
        exit 1
    }

    exit 0
}

# Startup information
Write-Host -NoNewline -Separator "" @(
    "`n",
    "$SEPARATOR`n"
)
Write-Host -NoNewline -Separator "" -ForegroundColor Green @(
    "`n",
    "Origin configuration & installation script v$VERSION`n"
)
Write-Host -NoNewline -Separator "" @(
    "`n",
    "This will help you configure Origin to not update automatically and optionally`n",
    "install the last known stable version to avoid being forced to migrate`n",
    "to the new EA app.`n",
    "`n",
    "See manual instructions and report issues at:`n",
    "https://github.com/alexitx/stop-origin-migration`n",
    "`n",
    "$SEPARATOR`n",
    "`n"
)

# --------------------
# Step 1 - Preparation
# --------------------

$Caption = "Proceed with configuration?"
$Message = -join @(
    "Yes - Terminate Origin if it is currently running`n",
    "      and proceed with the configuration`n",
    "No  - Quit"
)
$Decision = Show-Prompt -Caption $Caption -Message $Message
if ($Decision -ne 0) {
    exit 0
}

Write-Host "Stopping Origin processes"

$OriginProcesses = @(
    "Origin",
    "OriginClientService",
    "OriginWebHelperService",
    "OriginThinSetupInternal",
    "OriginSetup"
)
foreach ($Process in $OriginProcesses) {
    Stop-Process -Name $Process -Force -ErrorAction SilentlyContinue
}

# --------------------------------------
# Step 2 - Disable updates and migration
# --------------------------------------

$OriginConfigDir = Join-Path `
    -Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::CommonApplicationData)) `
    -ChildPath "Origin"
$OriginConfigFile = Join-Path -Path $OriginConfigDir -ChildPath "local.xml"

# Create config file if it doesn't exist
if (-not (Test-Path -Path $OriginConfigDir -PathType Container)) {
    Write-Host "Creating config directory '$OriginConfigDir'"

    New-Item -Path $OriginConfigDir -ItemType Directory | Out-Null
    Set-FileOwnership -Path $OriginConfigDir
}
if (-not (Test-Path -Path $OriginConfigFile -PathType Leaf)) {
    Write-Host "Creating config file '$OriginConfigFile'"

    New-Item -Path $OriginConfigFile -ItemType File | Out-Null
    Set-FileOwnership -Path $OriginConfigFile
}

# "Base" configuration with updates and migration disabled
$BaseConfigXml = [Xml.XmlDocument] -join @(
    "<?xml version='1.0' encoding='utf-8'?>`n",
    "<Settings>`n",
    "  <Setting key='AutoPatchGlobal' value='false' type='1'/>`n",
    "  <Setting key='AutoUpdate' value='false' type='1'/>`n",
    "  <Setting key='MigrationDisabled' value='true' type='1'/>`n",
    "  <Setting key='UpdateURL' value='' type='10'/>`n",
    "</Settings>`n"
)
$MigrationKeys = @("MigrationDisabled", "UpdateURL")

# Current Origin configuration
$ConfigXml = New-Object Xml.XmlDocument
$ConfigRaw = Get-Content -Path $OriginConfigFile
if (-not [String]::IsNullOrWhiteSpace($ConfigRaw)) {
    # Load raw config XML string
    $ConfigXml.LoadXml($ConfigRaw)
} else {
    # Create XML declaration and root element
    $Declaration = $ConfigXml.CreateXmlDeclaration("1.0", $null, $null)
    [void] $ConfigXml.InsertBefore($Declaration, $ConfigXml.DocumentElement)

    $RootElement = $ConfigXml.CreateElement("Settings")
    [void] $ConfigXml.AppendChild($RootElement)
}

$Caption = "Disable updates and migration?"
$Message = -join @(
    "Yes - Disable automatic updates and prevent forced migration`n",
    "No  - Allow migration"
)
$Decision = Show-Prompt -Caption $Caption -Message $Message
if ($Decision -eq -1) {
    exit 0
} elseif ($Decision -eq 0) {
    Write-Host "Disabling automatic updates and migration"
    Update-Settings -TargetXml $ConfigXml -BaseXml $BaseConfigXml
} else {
    Write-Host "Removing migration-blocking settings"
    Remove-Settings -TargetXml $ConfigXml -Keys $MigrationKeys
}

Write-Host "Saving modified configuration"

try {
    $UTF8NoBomEncoding = New-Object Text.UTF8Encoding($false)
    $ConfigStreamWriter = New-Object IO.StreamWriter($OriginConfigFile, $false, $UTF8NoBomEncoding)

    $ConfigXml.Save($ConfigStreamWriter)
} finally {
    if ($ConfigStreamWriter) {
        $ConfigStreamWriter.Dispose()
    }
}

# ----------------------------------------------
# Step 3 - Install the last known stable version
# ----------------------------------------------

$StableOriginVersion = "10.5.119.52718"
$OriginRegKey = "HKLM:\SOFTWARE\WOW6432Node\Origin"
$OriginVersionRegValue = "ClientVersion"

# Get the currently installed version
$InstalledOriginVersion = (
    Get-ItemProperty `
        -Path $OriginRegKey `
        -Name $OriginVersionRegValue `
        -ErrorAction SilentlyContinue
).$OriginVersionRegValue

$DefaultChoice = 0
if ($InstalledOriginVersion -eq $StableOriginVersion) {
    # Installed version is the same as the stable version
    Write-Host "Installed Origin version $InstalledOriginVersion found"
    $Caption = -join @(
        "You have the last known stable version of Origin already installed.`n",
        "Would you like to download and install it again anyway?"
    )
    $DefaultChoice = 1
} elseif ($InstalledOriginVersion) {
    # Installed version is different from the stable version
    Write-Host "Installed Origin version $InstalledOriginVersion found"
    $Caption = -join @(
        "The currently installed version of Origin is $StableOriginVersion.`n",
        "Would you like to download and install the last known`n",
        "stable version ${StableOriginVersion}?"
    )
} else {
    # No version value found or Origin is not installed
    Write-Host "No current Origin installation found"
    $Caption = -join @(
        "Would you like to download and install the last known`n",
        "stable version of Origin ${StableOriginVersion}?"
    )
}

$InstallerParts = @(
    @{
        Url = "https://raw.githubusercontent.com/alexitx/stop-origin-migration/master/setup/OriginSetup-10.5.119.52718.zip.001"
        Filename = "OriginSetup-10.5.119.52718.zip.001"
    },
    @{
        Url = "https://raw.githubusercontent.com/alexitx/stop-origin-migration/master/setup/OriginSetup-10.5.119.52718.zip.002"
        Filename = "OriginSetup-10.5.119.52718.zip.002"
    },
    @{
        Url = "https://raw.githubusercontent.com/alexitx/stop-origin-migration/master/setup/OriginSetup-10.5.119.52718.zip.003"
        Filename = "OriginSetup-10.5.119.52718.zip.003"
    }
)

$InstallerArchiveFilename = "OriginSetup-10.5.119.52718.zip"
$InstallerFilename = "OriginSetup-10.5.119.52718.exe"
$InstallerHash = "ed6ee5174f697744ac7c5783ff9021da603bbac42ae9836cd468d432cadc9779"

$ShouldInstall = $false

$Message = -join @(
    "Yes - Download and install Origin $StableOriginVersion`n",
    "No  - Skip installation"
)
$Decision = Show-Prompt -Caption $Caption -Message $Message -DefaultChoice $DefaultChoice
if ($Decision -eq -1) {
    exit 0
} elseif ($Decision -eq 0) {
    # Before downloading, check if a valid installer is already present in the script directory
    $FinalInstallerPath = Join-Path -Path $SCRIPT_DIR -ChildPath $InstallerFilename
    $ShouldDownload = $true

    if (Test-Path -Path $FinalInstallerPath) {
        Write-Host "Verifying installer integrity"

        $ComputedInstallerHash = (Get-FileHash -Path $FinalInstallerPath -Algorithm SHA256).Hash
        if ($ComputedInstallerHash -eq $InstallerHash) {
            Write-Host "Valid installer found at '$FinalInstallerPath'"
            $ShouldDownload = $false
            $ShouldInstall = $true
        } else {
            Write-Host "Installer integrity validation failed (SHA256: $ComputedInstallerHash)"
        }
    }

    if ($ShouldDownload) {
        try {
            $TempDir = New-TemporaryDirectory
            $TempInstallerPath = Join-Path -Path $TempDir -ChildPath $InstallerFilename

            # Download installer in multiple parts
            foreach ($Part in $InstallerParts) {
                $Url = $Part.Url
                $Destination = Join-Path -Path $TempDir -ChildPath $Part.Filename

                Write-Host "Downloading '$Url'"
                Invoke-DownloadFile -Uri $Url -Destination $Destination -MaxRetries 3
            }

            $InstallerArchiveParts = $InstallerParts | ForEach-Object {
                return Join-Path -Path $TempDir -ChildPath $_.Filename
            }
            $InstallerArchive = Join-Path -Path $TempDir -ChildPath $InstallerArchiveFilename

            # Concatenate installer parts to a single file
            Write-Host "Combining downloaded installer parts"
            Join-Files -Files $InstallerArchiveParts -Destination $InstallerArchive

            # Delete temporary installer archive parts
            $InstallerArchiveParts | ForEach-Object {
                Remove-Item -Path $_ -Force -ErrorAction SilentlyContinue
            }

            # Extract installer from archive
            Write-Host "Extracting installer"
            Expand-Archive -Path $InstallerArchive -DestinationPath $TempDir

            # Delete temporary installer archive
            Remove-Item -Path $InstallerArchive -Force -ErrorAction SilentlyContinue

            Write-Host "Verifying installer integrity"

            $ComputedInstallerHash = (Get-FileHash -Path $TempInstallerPath -Algorithm SHA256).Hash
            if ($ComputedInstallerHash -eq $InstallerHash) {
                Write-Host "Saving installer to '$FinalInstallerPath'"

                Copy-Item -Path $TempInstallerPath -Destination $FinalInstallerPath -Force
                Set-FileOwnership -Path $FinalInstallerPath

                $ShouldInstall = $true
            } else {
                Write-Host "Installer integrity validation failed (SHA256: $ComputedInstallerHash)"
                Write-Host -NoNewLine -Separator "" -ForegroundColor Red @(
                    "`n",
                    "The installation cannot continue because the downloaded installer is corrupt`n",
                    "or is the wrong version.`n",
                    "`n",
                    "Follow the instructions on the README page to download and install it manually."
                )
            }
        } finally {
            # Delete temporary directory
            if ($TempDir) {
                Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
    }

    if (-not $ShouldInstall) {
        exit 1
    }

    Write-Host "Launching installer"
    Start-Process -FilePath $FinalInstallerPath
}

# Final information
Write-Host -NoNewline -Separator "" @(
    "`n",
    "$SEPARATOR`n"
)
Write-Host -NoNewline -Separator "" -ForegroundColor Green @(
    "`n",
    "Done`n"
)
if ($ShouldInstall) {
    Write-Host -NoNewline -Separator "" @(
        "`n",
        "You can re-run the saved Origin installer manually without this script`n",
        "at any time.`n"
    )
}
Write-Host -NoNewline -Separator "" @(
    "`n",
    "To disable data collection, reduce resource usage and more, see:`n",
    "https://github.com/alexitx/stop-origin-migration#additional-configuration`n",
    "`n",
    "$SEPARATOR`n",
    "`n"
)