using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using AsmResolver.PE;
using AsmResolver.PE.File;
using AsmResolver.PE.File.Headers;
using AsmResolver.PE.Win32Resources.Builder;

namespace DeployAll;

public static class DotnetCmd
{
    private const string DotnetAppName = "dotnet";

    private const string DesiredRuntimeVersion = "8.0.0";
    
    public static void Publish(string projPath, string configuration, string runtime, string resultPath)
    {
        ProcessStartInfo psi = new ProcessStartInfo
        {
            FileName = DotnetAppName,
            ArgumentList =
            {
                "publish",
                projPath,
                "-c",
                configuration,
                "-clp:ErrorsOnly;Summary",
                "--self-contained",
                "-r",
                runtime,
                "/p:Platform=x64",
                "/p:ErrorOnDuplicatePublishOutputFiles=false", //TODO: fix our duplicate files
                "/p:RollForward=Disable",
                $"/p:RuntimeFrameworkVersion={DesiredRuntimeVersion}",
                "-o",
                resultPath
            },
            RedirectStandardOutput = true,
            RedirectStandardError = true
        };
        var process = Util.StartProcess(psi);
        process.WaitForExit();
        string stdout = process.StandardOutput.ReadToEnd();
        string stderr = process.StandardError.ReadToEnd();

        string errorLine = $"{stdout}\n{stderr}".Split('\n')
            .First(ln => ln.Contains("Error(s)", StringComparison.OrdinalIgnoreCase))
            .Trim();

        if (!errorLine.StartsWith("0 ", StringComparison.OrdinalIgnoreCase))
        {
            throw new Exception($"Failed to build {projPath}, {errorLine}");
        }
        
        Console.WriteLine($" - Published \"{projPath}\" to \"{resultPath}\", {errorLine}");

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || !runtime.StartsWith("win")) { return; }
        
        // You may be wondering, what is this crap?
        // Cross-compiling is something that should work perfectly, because it's super convenient.
        // However, thanks to the way .NET works, cross-compiling to Windows from *nix platforms
        // results in an executable with basically no metadata, and the wrong subsystem!
        // (see https://github.com/dotnet/sdk/blob/375955d3a9de213a01d70eb6180298000dee30ee/src/Tasks/Microsoft.NET.Build.Tasks/GenerateShims.cs#L127-L132)
        // Does it look like we're about to modify the SDK itself to solve this problem? Yeah right.
        // Instead let's just take the shim generated by the SDK and fix it ourselves.

        XElement firstPropertyGroup = XDocument.Load(projPath)
            .Root?
            .Element("PropertyGroup")
            ?? throw new Exception("PropertyGroup not found");

        string assemblyName = firstPropertyGroup.Element("AssemblyName")?.Value
            ?? throw new Exception("AssemblyName not found");
            
        // This is the shim that doesn't have the stuff we want.
        var fileToChange = PEFile.FromFile(Path.Combine(resultPath, $"{assemblyName}.exe"));
        // Luckily, the SDK does embed all of that data in the assembly with all of the IL!
        // We can just yoink it from here.
        var managedAssembly = PEImage.FromFile(Path.Combine(resultPath, $"{assemblyName}.dll"));
        
        // Here's a whole lot of magic to set up the resources section of the executable
        var resourceSection = new PESection(".rsrc", SectionFlags.ContentInitializedData | SectionFlags.MemoryRead);
        var resourceDirectoryBuffer = new ResourceDirectoryBuffer();
        resourceDirectoryBuffer.AddDirectory(managedAssembly.Resources ?? throw new Exception($"{assemblyName}.dll has no resources"));
        resourceSection.Contents = resourceDirectoryBuffer;
        fileToChange.Sections.Add(resourceSection);
        fileToChange.AlignSections();
        var dataDirectories = fileToChange.OptionalHeader.DataDirectories;
        dataDirectories[2] = new DataDirectory(resourceDirectoryBuffer.Rva, resourceDirectoryBuffer.GetPhysicalSize());

        // And here's something a little less magical that fixes the subsystem
        fileToChange.OptionalHeader.SubSystem = firstPropertyGroup.Element("OutputType")?.Value == "WinExe"
            ? SubSystem.WindowsGui
            : SubSystem.WindowsCui;

        using var writeStream = File.Open(Path.Combine(resultPath, $"{assemblyName}.exe"), FileMode.Create);
        fileToChange.Write(writeStream);
    }

    public static Version GetSdkVersion()
    {
        ProcessStartInfo psi = new ProcessStartInfo
        {
            FileName = DotnetAppName,
            ArgumentList =
            {
                "--version"
            },
            RedirectStandardOutput = true,
            RedirectStandardError = true
        };
        var process = Util.StartProcess(psi);
        process.WaitForExit();
        string stdout = process.StandardOutput.ReadToEnd();
        
        return Version.Parse(stdout.Trim());
    }
}
