#!/bin/pwsh
. "$PSScriptRoot/../../lib/Scripting.ps1";

function Start-Setup {
    param($ConfigurationName)
    . "$PSScriptRoot/../../lib/Settings.ps1";
    . "$PSScriptRoot/../../lib/Scripting.ps1";
    $Global:InformationPreference = "Continue";
    $Global:ErrorActionPreference = "Inquire";
    $env:CONFIG_NAME ??= $ConfigurationName;
    $null = $env:SETUP_SCRIPT_NAME ??= "$PSScriptRoot/Install.ps1";
    $env:WSLENV = "CONFIG_NAME";

    Show-ProfileNamePrompt;
    $valhallaConfig = ConvertFrom-Json (Get-Content "$PSScriptRoot/../../../.config/$env:CONFIG_NAME.json");
    [xml]$unattendedConfig = [xml]::new();
    $unattendedConfig.PreserveWhitespace = $true;

    $readerSettings = [System.Xml.XmlReaderSettings]::new();
    $readerSettings.IgnoreComments = $true;
    $reader = [System.Xml.XmlReader]::Create("$PSScriptRoot/../Resources/Autounattend.template.xml", $readerSettings);

    $unattendedConfig.Load($reader);
    $namespace = New-Object -TypeName "Xml.XmlNamespaceManager" -ArgumentList $unattendedConfig.NameTable;
    $namespace.AddNamespace("ua", $unattendedConfig.DocumentElement.NamespaceURI);

    function Get-InstallDisk {
        [int]$installTarget.Disk.InnerText;
    }

    function Get-PassSettings {
        [OutputType([xml])]
        param(
            [string] $passName
        )

        return $unattendedConfig.SelectSingleNode("/ua:unattend/ua:settings[@pass='$passName']", $namespace);
    }

    function Get-Component {
        [OutputType([xml])]
        param(
            [System.Xml.XmlNode] $pass,
            [string] $componentName = $null
        );

        $Local:selector = "./ua:component";

        if ($null -ne $componentName) {
            $Local:selector += "[@name='$componentName']";
        }

        return $pass.SelectSingleNode($Local:selector, $namespace);
    }

    function Get-RemoteScriptPath($path) {
        $relativePath = [System.IO.Path]::GetRelativePath("$PSScriptRoot/../../..", $path);
        Join-Path $env:REMOTE_PROJECT_PATH $relativePath;
    }

    function Get-PathInjection($path) {
        "(Join-Path `$env:SystemDrive $(ConvertTo-Injection $path))";
    }

    function Get-ScriptPathInjection($path) {
        Get-PathInjection (Get-RemoteScriptPath $path);
    }

    function Get-DiskConfig {
        param(
            [int] $Disk
        )

        $node = $disks | Where-Object { $_.SelectSingleNode("./ua:DiskID", $namespace).InnerText -eq $Disk };
        $creations = $node.SelectSingleNode("./ua:CreatePartitions", $namespace);
        $modifications = $node.SelectSingleNode("./ua:ModifyPartitions", $namespace);

        @{
            PartitionCreationContainer     = $creations;
            PartitionCreations             = $creations.SelectNodes("./ua:CreatePartition", $namespace);
            PartitionModificationContainer = $modifications;
            PartitionModifications         = $modifications.SelectNodes("./ua:ModifyPartition", $namespace);
        };
    }

    function Move-PartitionRange {
        param (
            [int] $Disk = (Get-InstallDisk),
            [Parameter(Position = 0)]
            [int] $From = 0,
            [Parameter(Position = 1)]
            [System.Nullable[int]] $To = $null,
            [Parameter(Position = 2)]
            [int] $By = 1
        )

        $diskInfo = Get-DiskConfig $Disk;

        if ((Get-InstallDisk) -eq $Disk) {
            $partition = [int]$installTarget.Partition.InnerText;

            if (($partition -ge $From) -and (($null -eq $To) -or ($partition -le $To))) {
                $installTarget.Partition.InnerText = "$($partition + $By)";
            }
        }

        foreach ($config in @(
                @($diskInfo.PartitionCreations, @("Order")),
                @($diskInfo.PartitionModifications, @("Order", "PartitionID")))) {
            foreach ($partition in $config[0]) {
                foreach ($property in $config[1]) {
                    $partitionNode = $partition.SelectSingleNode("./ua:$property", $namespace);
                    $partitionID = [int]$partitionNode.InnerText;
                    $newID = $partitionID;

                    if (($newID -ge $From) -and (($null -eq $To) -or ($newID -le $To))) {
                        $newID += $By;
                    }

                    if ($partitionID -ne $newID) {
                        $partitionNode.InnerText = "$newID";
                    }
                }
            }
        }
    }

    function Add-Partition {
        param (
            [int] $Disk = (Get-InstallDisk),
            [Parameter(Position = 0)]
            [int] $Index,
            [Parameter(Position = 1)]
            [int] $Size,
            [Parameter(Position = 2)]
            [string] $Type = "Primary"
        )

        $diskInfo = Get-DiskConfig $Disk;
        Move-PartitionRange -Disk $Disk -From $Index -By 1;

        $configs = @(
            @(
                $diskInfo.PartitionCreations,
                0,
                @(
                    @("Order", "$Index"),
                    @("Type", "$Type"),
                    @("Size", "$Size"))),
            @(
                $diskInfo.PartitionModifications,
                2,
                @(
                    @("Order", "$Index"),
                    @("PartitionID", "$Index")))
        );

        foreach ($config in $configs) {
            $partition = $config[0][$config[1]];
            $newPartition = $partition.CloneNode($true);

            foreach ($entry in $config[2]) {
                $newPartition.SelectSingleNode("./ua:$($entry[0])", $namespace).InnerText = $entry[1];
            }

            $null = $partition.ParentNode.AppendChild($newPartition);
        }
    }

    function Move-Partition {
        param (
            [int] $Disk = (Get-InstallDisk),
            [Parameter(Position = 0)]
            [int] $From,
            [Parameter(Position = 1)]
            [int] $To
        )

        Move-PartitionRange -Disk $Disk $From $From (-1 * ($From + 1));

        if ($From -gt $To) {
            Move-PartitionRange -Disk $Disk $To ($From - 1);
        }
        elseif ($From -lt $To) {
            Move-PartitionRange -Disk $Disk ($From + 1) $To - 1;
        }

        Move-PartitionRange -Disk $Disk -1 -1 ($To + 1)
    }

    function Add-StartupCommand {
        param(
            [string] $Script,
            [string] $Description
        )

        $installationCommand = $oobeSettings.SelectSingleNode("./ua:FirstLogonCommands/ua:SynchronousCommand[last()]", $namespace);
        $newCommand = $installationCommand.ParentNode.AppendChild($installationCommand.CloneNode($true));
        $newCommand.SelectSingleNode("./ua:CommandLine", $namespace).InnerText = $Script;
        $orderElement = $newCommand.SelectSingleNode("./ua:Order", $namespace);
        $orderElement.InnerText = ([int]($orderElement.InnerText) + 1);
        $newCommand.SelectSingleNode("./ua:Description", $namespace).InnerText = $Description;
    }

    # Collect necessary variables
    $winpePass = Get-PassSettings "windowsPE";
    $setupConfig = Get-Component $winpePass "Microsoft-Windows-Setup";
    $disks = $setupConfig.SelectNodes("./ua:DiskConfiguration/ua:Disk", $namespace);

    $installTarget = & {
        $target = $setupConfig.SelectSingleNode("./ua:ImageInstall/ua:OSImage/ua:InstallTo", $namespace);

        @{
            Disk      = $target.SelectSingleNode("./ua:DiskID", $namespace);
            Partition = $target.SelectSingleNode("./ua:PartitionID", $namespace);
        };
    };

    # Adjust unattended settings
    $computerName = (Get-Component (Get-PassSettings "specialize") "Microsoft-Windows-Shell-Setup").SelectSingleNode("./ua:ComputerName", $namespace);
    $computerName.InnerText = $valhallaConfig.hostname;

    # Execute corresponding installer script after startup
    $oobeSettings = (Get-Component (Get-PassSettings "oobeSystem") "Microsoft-Windows-Shell-Setup");

    foreach ($xpath in @("./ua:AutoLogon/ua:Username",
            "./ua:UserAccounts/ua:LocalAccounts/ua:LocalAccount/ua:Name",
            "./ua:UserAccounts/ua:LocalAccounts/ua:LocalAccount/ua:DisplayName")) {
        $oobeSettings.SelectSingleNode($xpath, $namespace).InnerText = $valhallaConfig.setupUser.name;
    }

    Add-StartupCommand `
        -Script (
            "powershell -Command " +
            ($env:DEBUG ? "`$env:DEBUG = $([int]$env:DEBUG);" : "") +
            "`$env:VALHALLA_ROOT = $(Get-PathInjection $env:REMOTE_PROJECT_PATH);" +
            "`$env:PWSH_PATH = $(Get-PathInjection $env:PWSH_PATH);" +
            "`$env:INSTALLER_SCRIPT = $(Get-ScriptPathInjection $env:SETUP_SCRIPT_NAME);" +
            "`$env:CONFIG_NAME = $(ConvertTo-Injection $env:CONFIG_NAME);" +
            "& (Join-Path `$env:PWSH_PATH pwsh) `$env:INSTALLER_SCRIPT;") `
        -Description "Install PowerShell Core and git and run setup script";

    if ($valhallaConfig.dualboot.enable) {
        $diskSize = [long](ConvertFrom-Csv (wmic diskdrive where "Index=$(Get-InstallDisk)" get Size | ForEach-Object { "$_".Trim(); })).Size;

        # Calculate Linux size
        $linuxSize = ([System.Math]::Floor(($diskSize * ($valhallaConfig.dualboot.linuxPercentage / 100)) / 1024 / 1024 / 1024)) * 1024;

        # Resize EFI partition to 1GB (for GRUB)
        (Get-DiskConfig (Get-InstallDisk)).PartitionCreations[1].SelectSingleNode("./ua:Size", $namespace).InnerText = "$(1024)";

        # Move boot partition to the beginning
        Move-Partition 2 1;

        # Add a Swap spacer
        $swapSpacer = 100;
        Add-Partition 2 $swapSpacer;
        Add-Partition 3 ($linuxSize - $swapSpacer);
    }

    $unattendedConfigFile = "X:\unattend.xml";
    $unattendedConfig.PreserveWhitespace = $true;
    $unattendedConfig.Save($unattendedConfigFile);

    Write-Warning "Attention: This program will completely wipe your current disk #1 and install Windows on it. Are you sure you want to do this?";
    Read-Host -Prompt "Hit enter to continue or CTRL+C to abort";
    & "$SETUP_DRIVE\setup.exe" /Unattend:$unattendedConfigFile;
}

Start-Setup @args;