// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Versioning;

namespace Microsoft.NET.Build.Tasks
{
    public class ResolveReadyToRunCompilers : TaskBase
    {
        public bool EmitSymbols { get; set; }
        public bool ReadyToRunUseCrossgen2 { get; set; }
        public string PerfmapFormatVersion { get; set; }

        [Required]
        public ITaskItem[] RuntimePacks { get; set; }
        public ITaskItem[] Crossgen2Packs { get; set; }
        [Required]
        public ITaskItem[] TargetingPacks { get; set; }
        [Required]
        public string RuntimeGraphPath { get; set; }
        [Required]
        public string NETCoreSdkRuntimeIdentifier { get; set; }

        [Output]
        public ITaskItem CrossgenTool { get; set; }
        [Output]
        public ITaskItem Crossgen2Tool { get; set; }

        internal struct CrossgenToolInfo
        {
            public string ToolPath;
            public string PackagePath;
            public string ClrJitPath;
            public string DiaSymReaderPath;
        }

        private ITaskItem _runtimePack;
        private ITaskItem _crossgen2Pack;
        private string _targetRuntimeIdentifier;
        private string _targetPlatform;
        private string _hostRuntimeIdentifier;

        private CrossgenToolInfo _crossgenTool;
        private CrossgenToolInfo _crossgen2Tool;

        private Architecture _targetArchitecture;
        private bool _crossgen2IsVersion5;

        protected override void ExecuteCore()
        {
            _runtimePack = GetNETCoreAppRuntimePack();
            _crossgen2Pack = Crossgen2Packs?.FirstOrDefault();
            _targetRuntimeIdentifier = _runtimePack?.GetMetadata(MetadataKeys.RuntimeIdentifier);

            // Get the list of runtime identifiers that we support and can target
            ITaskItem targetingPack = GetNETCoreAppTargetingPack();
            string supportedRuntimeIdentifiers = targetingPack?.GetMetadata(MetadataKeys.RuntimePackRuntimeIdentifiers);

            var runtimeGraph = new RuntimeGraphCache(this).GetRuntimeGraph(RuntimeGraphPath);
            var supportedRIDsList = supportedRuntimeIdentifiers == null ? Array.Empty<string>() : supportedRuntimeIdentifiers.Split(';');

            // Get the best RID for the host machine, which will be used to validate that we can run crossgen for the target platform and architecture
            _hostRuntimeIdentifier = NuGetUtils.GetBestMatchingRid(
                runtimeGraph,
                NETCoreSdkRuntimeIdentifier,
                supportedRIDsList,
                out _);

            if (_hostRuntimeIdentifier == null || _targetRuntimeIdentifier == null)
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return;
            }

            if (ReadyToRunUseCrossgen2)
            {
                if (!ValidateCrossgen2Support())
                {
                    return;
                }

                // In .NET 5 Crossgen2 did not support emitting native symbols, so we use Crossgen to emit them
                if (_crossgen2IsVersion5 && EmitSymbols && !ValidateCrossgenSupport())
                {
                    return;
                }
            }
            else
            {
                if (!ValidateCrossgenSupport())
                {
                    return;
                }
            }
        }

        private bool ValidateCrossgenSupport()
        {
            _crossgenTool.PackagePath = _runtimePack?.GetMetadata(MetadataKeys.PackageDirectory);
            if (_crossgenTool.PackagePath == null)
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return false;
            }

            if (!ExtractTargetPlatformAndArchitecture(_targetRuntimeIdentifier, out _targetPlatform, out _targetArchitecture) ||
                !ExtractTargetPlatformAndArchitecture(_hostRuntimeIdentifier, out string hostPlatform, out _) ||
                _targetPlatform != hostPlatform ||
                !GetCrossgenComponentsPaths())
            {
                Log.LogError(Strings.ReadyToRunTargetNotSupportedError);
                return false;
            }

            // Create tool task item
            CrossgenTool = new TaskItem(_crossgenTool.ToolPath);
            CrossgenTool.SetMetadata(MetadataKeys.JitPath, _crossgenTool.ClrJitPath);
            if (!string.IsNullOrEmpty(_crossgenTool.DiaSymReaderPath))
            {
                CrossgenTool.SetMetadata(MetadataKeys.DiaSymReader, _crossgenTool.DiaSymReaderPath);
            }

            return true;
        }

        private bool ValidateCrossgen2Support()
        {
            _crossgen2Tool.PackagePath = _crossgen2Pack?.GetMetadata(MetadataKeys.PackageDirectory);
            if (_crossgen2Tool.PackagePath == null ||
                !NuGetVersion.TryParse(_crossgen2Pack.GetMetadata(MetadataKeys.NuGetPackageVersion), out NuGetVersion crossgen2PackVersion))
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return false;
            }

            bool version5 = crossgen2PackVersion.Major < 6;
            bool isSupportedTarget = ExtractTargetPlatformAndArchitecture(_targetRuntimeIdentifier, out _targetPlatform, out _targetArchitecture);

            // Normalize target OS for crossgen invocation
            string targetOS = (_targetPlatform == "win") ? "windows" :
                // Map linux-{ musl,bionic,etc.} to linux
                _targetPlatform.StartsWith("linux-", StringComparison.Ordinal) ? "linux" :
                _targetPlatform;

            // In .NET 5 Crossgen2 supported only the following host->target compilation scenarios:
            //      win-x64 -> win-x64
            //      linux-x64 -> linux-x64
            //      linux-musl-x64 -> linux-musl-x64
            isSupportedTarget = isSupportedTarget &&
                (!version5 || _targetRuntimeIdentifier == _hostRuntimeIdentifier) &&
                GetCrossgen2ComponentsPaths(version5);

            if (!isSupportedTarget)
            {
                Log.LogError(Strings.ReadyToRunTargetNotSupportedError);
                return false;
            }

            // Create tool task item
            Crossgen2Tool = new TaskItem(_crossgen2Tool.ToolPath);
            Crossgen2Tool.SetMetadata(MetadataKeys.IsVersion5, version5.ToString());
            if (version5)
            {
                Crossgen2Tool.SetMetadata(MetadataKeys.JitPath, _crossgen2Tool.ClrJitPath);
            }
            else
            {
                Crossgen2Tool.SetMetadata(MetadataKeys.TargetOS, targetOS);
                Crossgen2Tool.SetMetadata(MetadataKeys.TargetArch, ArchitectureToString(_targetArchitecture));
                if (!string.IsNullOrEmpty(PerfmapFormatVersion))
                {
                    Crossgen2Tool.SetMetadata(MetadataKeys.PerfmapFormatVersion, PerfmapFormatVersion);
                }
            }

            _crossgen2IsVersion5 = version5;
            return true;
        }

        private ITaskItem GetNETCoreAppRuntimePack()
        {
            return GetNETCoreAppPack(RuntimePacks, MetadataKeys.FrameworkName);
        }

        private ITaskItem GetNETCoreAppTargetingPack()
        {
            return GetNETCoreAppPack(TargetingPacks, MetadataKeys.RuntimeFrameworkName);
        }

        private static ITaskItem GetNETCoreAppPack(ITaskItem[] packs, string metadataKey)
        {
            return packs.SingleOrDefault(
                pack => pack.GetMetadata(metadataKey)
                            .Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase));
        }

        private static bool ExtractTargetPlatformAndArchitecture(string runtimeIdentifier, out string platform, out Architecture architecture)
        {
            platform = null;
            architecture = default;

            // This will split RID like "linux-musl-arm64" into "linux-musl" and "arm64" components
            int separator = runtimeIdentifier.LastIndexOf('-');
            if (separator < 0)
            {
                return false;
            }

            string architectureStr = runtimeIdentifier.Substring(separator + 1).ToLowerInvariant();

            switch (architectureStr)
            {
                case "arm":
                    architecture = Architecture.Arm;
                    break;
                case "arm64":
                    architecture = Architecture.Arm64;
                    break;
                case "x64":
                    architecture = Architecture.X64;
                    break;
                case "x86":
                    architecture = Architecture.X86;
                    break;
                case "riscv64":
                    architecture = Architecture.RiscV64;
                    break;
                case "loongarch64":
                    architecture = Architecture.LoongArch64;
                    break;
                default:
                    return false;
            }

            platform = runtimeIdentifier.Substring(0, separator).ToLowerInvariant();
            return true;
        }

        private bool GetCrossgenComponentsPaths()
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                if (_targetArchitecture == Architecture.Arm)
                {
                    if (RuntimeInformation.OSArchitecture == _targetArchitecture)
                    {
                        // We can run native arm32 bits on an arm64 host in WOW mode
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.arm.dll");
                    }
                    else
                    {
                        // We can use the x86-hosted crossgen compiler to target ARM
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "x86_arm", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x86_arm", "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x86_arm", "native", "Microsoft.DiaSymReader.Native.x86.dll");
                    }
                }
                else if (_targetArchitecture == Architecture.Arm64)
                {
                    if (RuntimeInformation.OSArchitecture == _targetArchitecture)
                    {
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.arm64.dll");
                    }
                    else
                    {
                        // We only have 64-bit hosted compilers for ARM64.
                        if (RuntimeInformation.OSArchitecture != Architecture.X64)
                        {
                            return false;
                        }

                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "x64_arm64", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x64_arm64", "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x64_arm64", "native", "Microsoft.DiaSymReader.Native.amd64.dll");
                    }
                }
                else
                {
                    _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen.exe");
                    _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "clrjit.dll");
                    if (_targetArchitecture == Architecture.X64)
                    {
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.amd64.dll");
                    }
                    else
                    {
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.x86.dll");
                    }
                }
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                // Only x64 supported for OSX
                if (_targetArchitecture != Architecture.X64 || RuntimeInformation.OSArchitecture != Architecture.X64)
                {
                    return false;
                }

                _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen");
                _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "libclrjit.dylib");
            }
            else
            {
                // Generic Unix, including Linux and FreeBSD
                if (_targetArchitecture == Architecture.Arm || _targetArchitecture == Architecture.Arm64)
                {
                    if (RuntimeInformation.OSArchitecture == _targetArchitecture)
                    {
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "libclrjit.so");
                    }
                    else if (RuntimeInformation.OSArchitecture == Architecture.X64)
                    {
                        string xarchPath = (_targetArchitecture == Architecture.Arm ? "x64_arm" : "x64_arm64");
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", xarchPath, "crossgen");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", xarchPath, "native", "libclrjit.so");
                    }
                    else
                    {
                        return false;
                    }
                }
                else
                {
                    _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen");
                    _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "libclrjit.so");
                }
            }

            return File.Exists(_crossgenTool.ToolPath) && File.Exists(_crossgenTool.ClrJitPath);
        }

        private bool GetCrossgen2ComponentsPaths(bool version5)
        {
            string toolFileName, v5_clrJitFileNamePattern;

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                toolFileName = "crossgen2.exe";
                v5_clrJitFileNamePattern = "clrjit-{0}.dll";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                toolFileName = "crossgen2";
                v5_clrJitFileNamePattern = "libclrjit-{0}.dylib";
            }
            else
            {
                // Generic Unix, including Linux and FreeBSD
                toolFileName = "crossgen2";
                v5_clrJitFileNamePattern = "libclrjit-{0}.so";
            }

            if (version5)
            {
                string clrJitFileName = string.Format(v5_clrJitFileNamePattern, GetTargetSpecForVersion5());
                _crossgen2Tool.ClrJitPath = Path.Combine(_crossgen2Tool.PackagePath, "tools", clrJitFileName);
                if (!File.Exists(_crossgen2Tool.ClrJitPath))
                {
                    return false;
                }
            }

            _crossgen2Tool.ToolPath = Path.Combine(_crossgen2Tool.PackagePath, "tools", toolFileName);
            return File.Exists(_crossgen2Tool.ToolPath);
        }

        // Keep in sync with JitConfigProvider.GetTargetSpec in .NET 5
        private string GetTargetSpecForVersion5()
        {
            string targetOSComponent = (_targetPlatform == "win" ? "win" : "unix");
            string targetArchComponent = ArchitectureToString(_targetArchitecture);
            return targetOSComponent + '-' + targetArchComponent;
        }

        private static string ArchitectureToString(Architecture architecture)
        {
            return architecture switch
            {
                Architecture.X86 => "x86",
                Architecture.X64 => "x64",
                Architecture.Arm => "arm",
                Architecture.Arm64 => "arm64",
                Architecture.RiscV64 => "riscv64",
                Architecture.LoongArch64 => "loongarch64",
                _ => null
            };
        }
    }
}
