// 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 System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using WasmAppBuilder;

namespace Microsoft.WebAssembly.Build.Tasks;

public class ManagedToNativeGenerator : Task
{
    [Required]
    public string[] Assemblies { get; set; } = Array.Empty<string>();

    public string? RuntimeIcallTableFile { get; set; }

    public string? IcallOutputPath { get; set; }

    [Required, NotNull]
    public string[]? PInvokeModules { get; set; }

    [Required, NotNull]
    public string? PInvokeOutputPath { get; set; }

    [Required, NotNull]
    public string? InterpToNativeOutputPath { get; set; }
    public string? CacheFilePath { get; set; }

    public bool IsLibraryMode { get; set; }

    [Output]
    public string[]? FileWrites { get; private set; }

    public override bool Execute()
    {
        if (Assemblies!.Length == 0)
        {
            Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(Assemblies)} cannot be empty");
            return false;
        }

        if (PInvokeModules!.Length == 0)
        {
            Log.LogError($"{nameof(ManagedToNativeGenerator)}.{nameof(PInvokeModules)} cannot be empty");
            return false;
        }

        try
        {
            var logAdapter = new LogAdapter(Log);
            ExecuteInternal(logAdapter);
            return !Log.HasLoggedErrors;
        }
        catch (LogAsErrorException e)
        {
            Log.LogError(e.Message);
            return false;
        }
    }

    private void ExecuteInternal(LogAdapter log)
    {
        Dictionary<string, string> _symbolNameFixups = new();
        List<string> managedAssemblies = FilterOutUnmanagedBinaries(Assemblies);
        if (ShouldRun(managedAssemblies))
        {
            var pinvoke = new PInvokeTableGenerator(FixupSymbolName, log, IsLibraryMode);
            var icall = new IcallTableGenerator(RuntimeIcallTableFile, FixupSymbolName, log);

            var resolver = new PathAssemblyResolver(managedAssemblies);
            using var mlc = new MetadataLoadContext(resolver, "System.Private.CoreLib");
            foreach (string asmPath in managedAssemblies)
            {
                log.LogMessage(MessageImportance.Low, $"Loading {asmPath} to scan for pinvokes, and icalls");
                Assembly asm = mlc.LoadFromAssemblyPath(asmPath);
                pinvoke.ScanAssembly(asm);
                icall.ScanAssembly(asm);
            }

            IEnumerable<string> cookies = Enumerable.Concat(
                pinvoke.Generate(PInvokeModules, PInvokeOutputPath),
                icall.Generate(IcallOutputPath));

            var m2n = new InterpToNativeGenerator(log);
            m2n.Generate(cookies, InterpToNativeOutputPath);

            if (!string.IsNullOrEmpty(CacheFilePath))
                File.WriteAllLines(CacheFilePath, PInvokeModules);
        }

        List<string> fileWritesList = new() { PInvokeOutputPath, InterpToNativeOutputPath };
        if (IcallOutputPath != null)
            fileWritesList.Add(IcallOutputPath);
        if (!string.IsNullOrEmpty(CacheFilePath))
            fileWritesList.Add(CacheFilePath);

        FileWrites = fileWritesList.ToArray();

        string FixupSymbolName(string name)
        {
            if (_symbolNameFixups.TryGetValue(name, out string? fixedName))
                return fixedName;

            fixedName = Utils.FixupSymbolName(name);
            _symbolNameFixups[name] = fixedName;
            return fixedName;
        }

    }

    private bool ShouldRun(IList<string> managedAssemblies)
    {
        if (string.IsNullOrEmpty(CacheFilePath) || !File.Exists(CacheFilePath))
        {
            Log.LogMessage(MessageImportance.Low, $"Running because no cache file found at '{CacheFilePath}'.");
            return true;
        }

        string oldModules = string.Join(",", File.ReadLines(CacheFilePath).OrderBy(l => l));
        string newModules = string.Join(",", PInvokeModules.OrderBy(l => l));
        if (!string.Equals(oldModules, newModules, StringComparison.InvariantCulture))
        {
            Log.LogMessage(MessageImportance.Low, $"Running because the list of pinvoke modules has changed from: {oldModules} to {newModules} .");
            return true;
        }

        // compare against the output files
        // get the timestamp for oldest output file
        DateTime oldestOutputDt = DateTime.MaxValue;
        if (CheckShouldRunBecauseOfOutputFile(IcallOutputPath, ref oldestOutputDt) ||
            CheckShouldRunBecauseOfOutputFile(PInvokeOutputPath, ref oldestOutputDt) ||
            CheckShouldRunBecauseOfOutputFile(InterpToNativeOutputPath, ref oldestOutputDt))
        {
            return true;
        }

        foreach (string asm in managedAssemblies)
        {
            if (File.GetLastWriteTimeUtc(asm) >= oldestOutputDt)
            {
                Log.LogMessage(MessageImportance.Low, $"Running because {asm} is newer than one of the output files.");
                return true;
            }
        }

        Log.LogMessage(MessageImportance.Low, $"Skipping because all of the assemblies are older than the output files.");
        return false;

        bool CheckShouldRunBecauseOfOutputFile(string? path, ref DateTime oldestDt)
        {
            if (string.IsNullOrEmpty(path))
                return false;

            if (!File.Exists(path))
            {
                Log.LogMessage(MessageImportance.Low, $"Running because output file '{path}' does not exist.");
                return true;
            }
            DateTime utc = File.GetLastWriteTimeUtc(path);
            oldestDt = utc < oldestDt ? utc : oldestDt;
            return false;
        }
    }

    private List<string> FilterOutUnmanagedBinaries(string[] assemblies)
    {
        List<string> managedAssemblies = new(assemblies.Length);
        foreach (string asmPath in Assemblies)
        {
            if (!File.Exists(asmPath))
                throw new LogAsErrorException($"Cannot find assembly {asmPath}");

            try
            {
                if (!Utils.IsManagedAssembly(asmPath))
                {
                    Log.LogMessage(MessageImportance.Low, $"Skipping unmanaged {asmPath}.");
                    continue;
                }
            }
            catch (Exception ex)
            {
                Log.LogMessage(MessageImportance.Low, $"Failed to read assembly {asmPath}: {ex}");
                throw new LogAsErrorException($"Failed to read assembly {asmPath}: {ex.Message}");
            }

            managedAssemblies.Add(asmPath);
        }

        return managedAssemblies;
    }
}
