// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;

#nullable disable

namespace Microsoft.Build.Tasks
{
    /// <summary>
    ///  Gathers the list of installed SDKS in the registry and on disk and outputs them into the project
    ///  so they can be used during SDK reference resolution and RAR for single files.
    /// </summary>
#pragma warning disable RS0022 // Constructor make noninheritable base class inheritable: Longstanding API design that we shouldn't change now
    public class GetInstalledSDKLocations : TaskExtension
#pragma warning restore RS0022 // Constructor make noninheritable base class inheritable
    {
        /// <summary>
        /// Metadata name for directory roots on installed SDK items
        /// </summary>
        internal const string DirectoryRootsMetadataName = "DirectoryRoots";

        /// <summary>
        /// Metadata name for extension directory roots on installed SDK items
        /// </summary>
        internal const string ExtensionDirectoryRootsMetadataName = "ExtensionDirectoryRoots";

        /// <summary>
        /// Metadata name for SDK Name
        /// </summary>
        internal const string SDKNameMetadataName = "SDKName";

        /// <summary>
        /// Metadata name for registry roots on installed SDK items
        /// </summary>
        internal const string RegistryRootMetadataName = "RegistryRoot";

        /// <summary>
        /// Key into our build cache
        /// </summary>
        private const string StaticSDKCacheKey = "StaticToolLocationHelperSDKCacheDisposer";

        #region Properties

        /// <summary>
        /// Target platform version
        /// </summary>
        private string _targetPlatformVersion = String.Empty;

        /// <summary>
        /// Target platform identifier
        /// </summary>
        private string _targetPlatformIdentifier = String.Empty;

        /// <summary>
        /// Platform version we are targeting
        /// </summary>
        [Required]
        public string TargetPlatformVersion
        {
            get => _targetPlatformVersion;

            set
            {
                ErrorUtilities.VerifyThrowArgumentNull(value, nameof(TargetPlatformVersion));
                _targetPlatformVersion = value;
            }
        }

        /// <summary>
        /// Platform identifier we are targeting
        /// </summary>
        [Required]
        public string TargetPlatformIdentifier
        {
            get => _targetPlatformIdentifier;

            set
            {
                ErrorUtilities.VerifyThrowArgumentNull(value, nameof(TargetPlatformIdentifier));
                _targetPlatformIdentifier = value;
            }
        }

        /// <summary>
        /// Root registry root to look for SDKs
        /// </summary>
        public string SDKRegistryRoot { get; set; }

        /// <summary>
        /// Root directory on disk to look for SDKs
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SDK", Justification = "Shipped this way in Dev11 Beta (go-live)")]
        public string[] SDKDirectoryRoots { get; set; }

        /// <summary>
        /// Root directories on disk to look for new style extension SDKs
        /// </summary>
        public string[] SDKExtensionDirectoryRoots { get; set; }

        /// <summary>
        /// When set to true, the task will produce a warning if there were no SDKs found.
        /// </summary>
        public bool WarnWhenNoSDKsFound { get; set; } = true;

        /// <summary>
        /// Set of items that represent all of the installed SDKs found in the SDKDirectory and SDKRegistry roots.
        /// The itemspec is the SDK install location. There is a piece of metadata called SDKName which contains the name of the SDK.
        /// </summary>
        [Output]
        public ITaskItem[] InstalledSDKs { get; set; }
        #endregion

        #region ITask Members

        /// <summary>
        /// Get the SDK.
        /// </summary>
        public override bool Execute()
        {
            if (!NativeMethodsShared.IsWindows)
            {
                Log.LogErrorWithCodeFromResources("General.TaskRequiresWindows", nameof(GetInstalledSDKLocations));
                return false;
            }

            // TargetPlatformVersion and TargetPlatformIdentifier are requried to correctly look for SDKs.
            if (String.IsNullOrEmpty(TargetPlatformVersion) || String.IsNullOrEmpty(TargetPlatformIdentifier))
            {
                Log.LogErrorWithCodeFromResources("GetInstalledSDKs.TargetPlatformInformationMissing");
                return false;
            }

            // Dictionary of ESDKs. Each entry is a (location, platform version) tuple
            IDictionary<string, Tuple<string, string>> installedSDKs = null;

            try
            {
                Log.LogMessageFromResources("GetInstalledSDKs.SearchingForSDKs", _targetPlatformIdentifier, _targetPlatformVersion);

                Version platformVersion = Version.Parse(TargetPlatformVersion);
                installedSDKs = ToolLocationHelper.GetPlatformExtensionSDKLocationsAndVersions(SDKDirectoryRoots, SDKExtensionDirectoryRoots, SDKRegistryRoot, TargetPlatformIdentifier, platformVersion);
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                Log.LogErrorWithCodeFromResources("GetInstalledSDKs.CouldNotGetSDKList", e.Message);
            }

            var outputItems = new List<ITaskItem>();

            if (installedSDKs?.Count > 0)
            {
                Log.LogMessageFromResources(MessageImportance.Low, "GetInstalledSDKs.FoundSDKs", installedSDKs.Count);
                Log.LogMessageFromResources(MessageImportance.Low, "GetInstalledSDKs.ListInstalledSDKs");

                foreach (KeyValuePair<string, Tuple<string, string>> sdk in installedSDKs)
                {
                    string sdkInfo = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("GetInstalledSDKs.SDKNameAndLocation", sdk.Key, sdk.Value.Item1);
                    Log.LogMessageFromResources(MessageImportance.Low, "ResolveAssemblyReference.FourSpaceIndent", sdkInfo);

                    var item = new TaskItem(sdk.Value.Item1);
                    item.SetMetadata("SDKName", sdk.Key);
                    item.SetMetadata("PlatformVersion", sdk.Value.Item2);

                    // Need to stash these so we can unroll the platform via GetMatchingPlatformSDK when we get the reference files for the sdks
                    item.SetMetadata(DirectoryRootsMetadataName, String.Join(";", SDKDirectoryRoots ?? []));
                    item.SetMetadata(ExtensionDirectoryRootsMetadataName, String.Join(";", SDKExtensionDirectoryRoots ?? []));
                    item.SetMetadata(RegistryRootMetadataName, SDKRegistryRoot);

                    outputItems.Add(item);
                }
            }
            else
            {
                if (WarnWhenNoSDKsFound)
                {
                    Log.LogWarningWithCodeFromResources("GetInstalledSDKs.NoSDksFound", TargetPlatformIdentifier, TargetPlatformVersion);
                }
            }

            InstalledSDKs = outputItems.ToArray();

            // We need to register an object so that at the end of the build we will clear the static toolLocationhelper caches.
            // this is important because if someone adds an SDK between builds we would not know about it and not be able to use it.
            // This code is mainly used to deal with the case where msbuild nodes hang around between builds.
            if (BuildEngine is IBuildEngine4 buildEngine4)
            {
                object staticCacheDisposer = buildEngine4.GetRegisteredTaskObject(StaticSDKCacheKey, RegisteredTaskObjectLifetime.Build);
                if (staticCacheDisposer == null)
                {
#pragma warning disable CA2000 // Dispose objects before losing scope is suppressed because the object is registered with the engine and disposed of at the end of the build.
                    BuildCacheDisposeWrapper staticDisposer = new BuildCacheDisposeWrapper(ToolLocationHelper.ClearSDKStaticCache);
#pragma warning restore CA2000 // Dispose objects before losing scope
                    buildEngine4.RegisterTaskObject(StaticSDKCacheKey, staticDisposer, RegisteredTaskObjectLifetime.Build, allowEarlyCollection: false);
                }
            }

            return !Log.HasLoggedErrors;
        }

        #endregion
    }
}
