// 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.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Reflection.PortableExecutable;
using System.Reflection.Metadata;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

internal static class Utils
{
    public enum HashAlgorithmType
    {
        SHA256,
        SHA384,
        SHA512
    };

    public enum HashEncodingType
    {
        Base64,
        Base64Safe
    };

    public static string WebcilInWasmExtension = ".wasm";

    private static readonly object s_SyncObj = new object();

    private static readonly char[] s_charsToReplace = new[] { '.', '-', '+', '<', '>' };

    public static string GetEmbeddedResource(string file)
    {
        using Stream stream = typeof(Utils).Assembly
            .GetManifestResourceStream($"{typeof(Utils).Assembly.GetName().Name}.Templates.{file}")!;
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }

    public static bool IsNewerThan(string inFile, string outFile)
        => !File.Exists(inFile) || !File.Exists(outFile) ||
                (File.GetLastWriteTimeUtc(inFile) > File.GetLastWriteTimeUtc(outFile));

    public static (int exitCode, string output) RunShellCommand(
                                        TaskLoggingHelper logger,
                                        string command,
                                        IDictionary<string, string> envVars,
                                        string workingDir,
                                        bool silent=false,
                                        bool logStdErrAsMessage=false,
                                        MessageImportance debugMessageImportance=MessageImportance.Low,
                                        string? label=null)
    {
        string scriptFileName = CreateTemporaryBatchFile(command);
        (string shell, string args) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                                                    ? ("cmd", $"/c \"{scriptFileName}\"")
                                                    : ("/bin/sh", $"\"{scriptFileName}\"");

        string msgPrefix = label == null ? string.Empty : $"[{label}] ";
        logger.LogMessage(debugMessageImportance, $"{msgPrefix}Running {command} via script {scriptFileName}:", msgPrefix);
        logger.LogMessage(debugMessageImportance, File.ReadAllText(scriptFileName), msgPrefix);

        return TryRunProcess(logger,
                             shell,
                             args,
                             envVars,
                             workingDir,
                             silent: silent,
                             logStdErrAsMessage: logStdErrAsMessage,
                             label: label,
                             debugMessageImportance: debugMessageImportance);

        static string CreateTemporaryBatchFile(string command)
        {
            string extn = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : ".sh";
            string file = Path.Combine(Path.GetTempPath(), $"tmp{Guid.NewGuid():N}{extn}");

            using StreamWriter sw = new(file);
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // set encoding to UTF-8 -> full Unicode support is needed for usernames -
                // `command` contains tmp dir path with the username
                sw.WriteLine(@"%SystemRoot%\System32\chcp.com 65001>nul");
                sw.WriteLine("setlocal");
                sw.WriteLine("set errorlevel=dummy");
                sw.WriteLine("set errorlevel=");
            }
            else
            {
                // Use sh rather than bash, as not all 'nix systems necessarily have Bash installed
                sw.WriteLine("#!/bin/sh");
            }

            sw.WriteLine(command);
            return file;
        }
    }

    public static string RunProcess(
        TaskLoggingHelper logger,
        string path,
        string args = "",
        IDictionary<string, string>? envVars = null,
        string? workingDir = null,
        bool ignoreErrors = false,
        bool silent = true,
        MessageImportance debugMessageImportance=MessageImportance.High)
    {
        (int exitCode, string output) = TryRunProcess(
                                            logger,
                                            path,
                                            args,
                                            envVars,
                                            workingDir,
                                            silent: silent,
                                            debugMessageImportance: debugMessageImportance);

        if (exitCode != 0 && !ignoreErrors)
            throw new Exception("Error: Process returned non-zero exit code: " + output);

        return output;
    }

    public static (int, string) TryRunProcess(
        TaskLoggingHelper logger,
        string path,
        string args = "",
        IDictionary<string, string>? envVars = null,
        string? workingDir = null,
        bool silent = true,
        bool logStdErrAsMessage = false,
        MessageImportance debugMessageImportance=MessageImportance.High,
        string? label=null,
        Action<Stream>? inputProvider = null)
    {
        string msgPrefix = label == null ? string.Empty : $"[{label}] ";
        logger.LogMessage(debugMessageImportance, $"{msgPrefix}Running: {path} {args}");
        var outputBuilder = new StringBuilder();
        var processStartInfo = new ProcessStartInfo
        {
            FileName = path,
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            RedirectStandardInput = inputProvider != null,
            Arguments = args,
        };

        if (workingDir != null)
            processStartInfo.WorkingDirectory = workingDir;

        logger.LogMessage(debugMessageImportance, $"{msgPrefix}Using working directory: {workingDir ?? Environment.CurrentDirectory}", msgPrefix);

        if (envVars != null)
        {
            if (envVars.Count > 0)
                logger.LogMessage(MessageImportance.Low, $"{msgPrefix}Setting environment variables for execution:", msgPrefix);

            foreach (KeyValuePair<string, string> envVar in envVars)
            {
                processStartInfo.EnvironmentVariables[envVar.Key] = envVar.Value;
                logger.LogMessage(MessageImportance.Low, $"{msgPrefix}\t{envVar.Key} = {envVar.Value}");
            }
        }

        Process? process = Process.Start(processStartInfo);
        if (process == null)
            throw new ArgumentException($"{msgPrefix}Process.Start({path} {args}) returned null process");

        process.ErrorDataReceived += (sender, e) =>
        {
            lock (s_SyncObj)
            {
                if (string.IsNullOrEmpty(e.Data))
                    return;

                string msg = $"{msgPrefix}{e.Data}";
                if (!silent)
                {
                    if (logStdErrAsMessage)
                        logger.LogMessage(debugMessageImportance, e.Data, msgPrefix);
                    else
                        logger.LogWarning(msg);
                }
                outputBuilder.AppendLine(e.Data);
            }
        };
        process.OutputDataReceived += (sender, e) =>
        {
            lock (s_SyncObj)
            {
                if (string.IsNullOrEmpty(e.Data))
                    return;

                if (!silent)
                    logger.LogMessage(debugMessageImportance, e.Data, msgPrefix);
                outputBuilder.AppendLine(e.Data);
            }
        };
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();
        inputProvider?.Invoke(process.StandardInput.BaseStream);
        process.WaitForExit();

        logger.LogMessage(debugMessageImportance, $"{msgPrefix}Exit code: {process.ExitCode}");
        return (process.ExitCode, outputBuilder.ToString().Trim('\r', '\n'));
    }

    public static bool CopyIfDifferent(string src, string dst, bool useHash)
    {
        if (!File.Exists(src))
            throw new ArgumentException($"Cannot find {src} file to copy", nameof(src));

        bool areDifferent = !File.Exists(dst) ||
                                (useHash && ComputeHash(src) != ComputeHash(dst)) ||
                                (File.ReadAllText(src) != File.ReadAllText(dst));

        if (areDifferent)
            File.Copy(src, dst, true);

        return areDifferent;
    }

    private static string ToBase64SafeString(byte[] data)
    {
        if (data.Length == 0)
            return string.Empty;

        int outputLength = ((4 * data.Length / 3) + 3) & ~3;
        char[] base64Safe = new char[outputLength];
        int base64SafeLength = Convert.ToBase64CharArray(data, 0, data.Length, base64Safe, 0, Base64FormattingOptions.None);

        //RFC3548, URL and Filename Safe Alphabet.
        for (int i = 0; i < base64SafeLength; i++)
        {
            if (base64Safe[i] == '+')
                base64Safe[i] = '-';
            else if (base64Safe[i] == '/')
                base64Safe[i] = '_';
        }

        return new string(base64Safe);
    }

    private static byte[] ComputeHashFromStream(Stream stream, HashAlgorithmType algorithm)
    {
        if (algorithm == HashAlgorithmType.SHA512)
        {
            using HashAlgorithm hashAlgorithm = SHA512.Create();
            return hashAlgorithm.ComputeHash(stream);
        }
        else if (algorithm == HashAlgorithmType.SHA384)
        {
            using HashAlgorithm hashAlgorithm = SHA384.Create();
            return hashAlgorithm.ComputeHash(stream);
        }
        else if (algorithm == HashAlgorithmType.SHA256)
        {
            using HashAlgorithm hashAlgorithm = SHA256.Create();
            return hashAlgorithm.ComputeHash(stream);
        }
        else
        {
            throw new ArgumentException($"Unsupported hash algorithm: {algorithm}");
        }
    }

    private static string EncodeHash(byte[] data, HashEncodingType encoding)
    {
        if (encoding == HashEncodingType.Base64)
        {
            return Convert.ToBase64String(data);
        }
        else if (encoding == HashEncodingType.Base64Safe)
        {
            return ToBase64SafeString(data);
        }
        else
        {
            throw new ArgumentException($"Unsupported hash encoding: {encoding}");
        }
    }

    public static string ComputeHash(string filepath)
    {
        return ComputeHashEx(filepath);
    }

    public static string ComputeHashEx(string filepath, HashAlgorithmType algorithm = HashAlgorithmType.SHA512, HashEncodingType encoding = HashEncodingType.Base64)
    {
        using var stream = File.OpenRead(filepath);
        return EncodeHash(ComputeHashFromStream(stream, algorithm), encoding);
    }

    public static string ComputeIntegrity(string filepath)
    {
        using var stream = File.OpenRead(filepath);
        using HashAlgorithm hashAlgorithm = SHA256.Create();

        byte[] hash = hashAlgorithm.ComputeHash(stream);
        return "sha256-" + Convert.ToBase64String(hash);
    }

    public static string ComputeIntegrity(byte[] bytes)
    {
        using HashAlgorithm hashAlgorithm = SHA256.Create();

        byte[] hash = hashAlgorithm.ComputeHash(bytes);
        return "sha256-" + Convert.ToBase64String(hash);
    }

    public static string ComputeTextIntegrity(string str)
    {
        using HashAlgorithm hashAlgorithm = SHA256.Create();

        var bytes = Encoding.UTF8.GetBytes(str);
        byte[] hash = hashAlgorithm.ComputeHash(bytes);
        return "sha256-" + Convert.ToBase64String(hash);
    }

#if NET
    public static void DirectoryCopy(string sourceDir, string destDir, Func<string, bool>? predicate=null)
    {
        if (!Directory.Exists(destDir))
            Directory.CreateDirectory(destDir);

        string[] files = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories);
        foreach (string file in files)
        {
            if (predicate != null && !predicate(file))
                continue;

            string relativePath = Path.GetRelativePath(sourceDir, file);
            string? relativeDir = Path.GetDirectoryName(relativePath);
            if (!string.IsNullOrEmpty(relativeDir))
                Directory.CreateDirectory(Path.Combine(destDir, relativeDir));

            File.Copy(file, Path.Combine(destDir, relativePath), true);
        }
    }
#endif

    public static bool IsWindows()
    {
#if NET
        return OperatingSystem.IsWindows();
#else
        return true;
#endif
    }

    public static bool IsMacOS()
    {
#if NET
        return OperatingSystem.IsMacOS();
#else
        return false;
#endif
    }

    public static bool IsLinux()
    {
#if NET
        return OperatingSystem.IsLinux();
#else
        return false;
#endif
    }

    public static bool IsManagedAssembly(string filePath)
    {
        if (!File.Exists(filePath))
            return false;

        // Try to read CLI metadata from the PE file.
        using FileStream fileStream = File.OpenRead(filePath);
        using PEReader peReader = new(fileStream, PEStreamOptions.Default);
        return IsManagedAssembly(peReader);
    }

    public static bool IsManagedAssembly(byte[] bytes)
    {
        using var peReader = new PEReader(ImmutableArray.Create(bytes));
        return IsManagedAssembly(peReader);
    }

    private static bool IsManagedAssembly(PEReader peReader)
    {
        try
        {
            if (!peReader.HasMetadata)
            {
                return false; // File does not have CLI metadata.
            }

            // Check that file has an assembly manifest.
            MetadataReader reader = peReader.GetMetadataReader();
            return reader.IsAssembly;
        }
        catch (BadImageFormatException)
        {
            return false;
        }
        catch (FileNotFoundException)
        {
            return false;
        }
    }

    // Keep synced with mono_fixup_symbol_name from src/mono/mono/metadata/native-library.c
    public static string FixupSymbolName(string name)
    {
        UTF8Encoding utf8 = new();
        byte[] bytes = utf8.GetBytes(name);
        StringBuilder sb = new();

        foreach (byte b in bytes)
        {
            if ((b >= (byte)'0' && b <= (byte)'9') ||
                (b >= (byte)'a' && b <= (byte)'z') ||
                (b >= (byte)'A' && b <= (byte)'Z') ||
                (b == (byte)'_'))
            {
                sb.Append((char)b);
            }
            else if (s_charsToReplace.Contains((char)b))
            {
                sb.Append('_');
            }
            else
            {
                sb.Append($"_{b:X}_");
            }
        }

        return sb.ToString();
    }
}
