﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Security;

namespace Microsoft.CodeAnalysis.BuildTasks
{
    /// <summary>
    /// General utilities.
    /// </summary>
    internal static class Utilities
    {
        /// <summary>
        /// Copied from msbuild. ItemSpecs are normalized using this method.
        /// </summary>
        public static string FixFilePath(string path)
            => string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/');

        /// <summary>
        /// Convert a task item metadata to bool. Throw an exception if the string is badly formed and can't
        /// be converted.
        /// 
        /// If the metadata is not found, then set metadataFound to false and then return false.
        /// </summary>
        /// <param name="item">The item that contains the metadata.</param>
        /// <param name="itemMetadataName">The name of the metadata.</param>
        /// <returns>The resulting boolean value.</returns>
        internal static bool TryConvertItemMetadataToBool(ITaskItem item, string itemMetadataName)
        {
            string metadataValue = item.GetMetadata(itemMetadataName);
            if (metadataValue == null || metadataValue.Length == 0)
            {
                return false;
            }

            try
            {
                return ConvertStringToBool(metadataValue);
            }
            catch (System.ArgumentException e)
            {
                throw Utilities.GetLocalizedArgumentException(
                    e,
                    ErrorString.General_InvalidAttributeMetadata,
                    item.ItemSpec, itemMetadataName, metadataValue, "bool");
            }
        }

        /// <summary>
        /// Converts a string to a bool.  We consider "true/false", "on/off", and 
        /// "yes/no" to be valid boolean representations in the XML.
        /// </summary>
        /// <param name="parameterValue">The string to convert.</param>
        /// <returns>Boolean true or false, corresponding to the string.</returns>
        internal static bool ConvertStringToBool(string parameterValue)
        {
            if (ValidBooleanTrue(parameterValue))
            {
                return true;
            }
            else if (ValidBooleanFalse(parameterValue))
            {
                return false;
            }
            else
            {
                // Unsupported boolean representation.
                throw Utilities.GetLocalizedArgumentException(
                    ErrorString.General_CannotConvertStringToBool,
                    parameterValue);
            }
        }
        /// <summary>
        /// Returns true if the string can be successfully converted to a bool,
        /// such as "on" or "yes"
        /// </summary>
        internal static bool CanConvertStringToBool(string parameterValue) =>
            ValidBooleanTrue(parameterValue) || ValidBooleanFalse(parameterValue);

        /// <summary>
        /// Returns true if the string represents a valid MSBuild boolean true value,
        /// such as "on", "!false", "yes"
        /// </summary>
        private static bool ValidBooleanTrue(string parameterValue) =>
            String.Compare(parameterValue, "true", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "on", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "yes", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "!false", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "!off", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "!no", StringComparison.OrdinalIgnoreCase) == 0;

        /// <summary>
        /// Returns true if the string represents a valid MSBuild boolean false value,
        /// such as "!on" "off" "no" "!true"
        /// </summary>
        private static bool ValidBooleanFalse(string parameterValue) =>
            String.Compare(parameterValue, "false", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "off", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "no", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "!true", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "!on", StringComparison.OrdinalIgnoreCase) == 0 ||
            String.Compare(parameterValue, "!yes", StringComparison.OrdinalIgnoreCase) == 0;

        internal static string GetFullPathNoThrow(string path)
        {
            try
            {
                path = Path.GetFullPath(path);
            }
            catch (Exception e) when (IsIoRelatedException(e)) { }
            return path;
        }

        internal static void DeleteNoThrow(string path)
        {
            try
            {
                File.Delete(path);
            }
            catch (Exception e) when (IsIoRelatedException(e)) { }
        }

        internal static bool IsIoRelatedException(Exception e) =>
            e is UnauthorizedAccessException ||
            e is NotSupportedException ||
            (e is ArgumentException && !(e is ArgumentNullException)) ||
            e is SecurityException ||
            e is IOException;

        internal static Exception GetLocalizedArgumentException(Exception e,
                                                                string errorString,
                                                                params object[] args)
        {
            return new ArgumentException(string.Format(CultureInfo.CurrentCulture, errorString, args), e);
        }

        internal static Exception GetLocalizedArgumentException(string errorString,
                                                                params object[] args)
        {
            return new ArgumentException(string.Format(CultureInfo.CurrentCulture, errorString, args));
        }

        internal static string? TryGetAssemblyPath(Assembly assembly)
        {
#if NETFRAMEWORK
            if (assembly.GlobalAssemblyCache)
            {
                return null;
            }

            if (assembly.CodeBase is { } codebase)
            {
                var uri = new Uri(codebase);
                return uri.IsFile ? uri.LocalPath : assembly.Location;
            }

            return null;
#else
            return assembly.Location;
#endif
        }

        /// <summary>
        /// Generate the full path to the tool that is deployed with our build tasks.
        /// </summary>
        internal static string GenerateFullPathToTool(string toolFileName)
        {
            var buildTask = typeof(Utilities).GetTypeInfo().Assembly;
            var assemblyPath = buildTask.Location;
            var assemblyDirectory = Path.GetDirectoryName(assemblyPath)!;

            return RuntimeHostInfo.IsDesktopRuntime
                ? Path.Combine(assemblyDirectory, toolFileName)
                : Path.Combine(assemblyDirectory, "bincore", toolFileName);
        }
    }
}
