// 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 Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.WebAssembly.Build.Tasks;

public class WasmLoadAssembliesAndReferences : Task
{
    [Required]
    [NotNull]
    public string[]? Assemblies { get; set; }

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

    // If true, continue when a referenced assembly cannot be found.
    public bool SkipMissingAssemblies { get; set; }

    // The set of assemblies the app will use
    [Output]
    public string[]? ReferencedAssemblies { get; private set; }

    private SortedDictionary<string, Assembly> _assemblies = new();

    public override bool Execute ()
    {
        string? badPath = AssemblySearchPaths.FirstOrDefault(path => !Directory.Exists(path));
        if (badPath != null)
        {
            Log.LogError($"Directory '{badPath}' in AssemblySearchPaths does not exist or is not a directory.");
            return false;
        }

        SearchPathsAssemblyResolver resolver = new(AssemblySearchPaths);
        MetadataLoadContext mlc = new(resolver, "System.Private.CoreLib");
        foreach (var asm in Assemblies)
        {
            var asmFullPath = Path.GetFullPath(asm);
            if (!File.Exists(asmFullPath))
            {
                Log.LogError($"Could not find assembly '{asmFullPath}'");
                return false;
            }

            var refAssembly = mlc.LoadFromAssemblyPath(asmFullPath);
            if (!AddAssemblyAndReferences(mlc, refAssembly))
                return !Log.HasLoggedErrors;
        }

        ReferencedAssemblies = _assemblies.Values.Select(asm => asm.Location).ToArray();
        return !Log.HasLoggedErrors;
    }

    private bool AddAssemblyAndReferences(MetadataLoadContext mlc, Assembly assembly)
    {
        if (_assemblies.ContainsKey(assembly.GetName().Name!))
            return true;

        _assemblies[assembly.GetName().Name!] = assembly;
        foreach (var aname in assembly.GetReferencedAssemblies())
        {
            try
            {
                Assembly refAssembly = mlc.LoadFromAssemblyName(aname);
                if (!AddAssemblyAndReferences(mlc, refAssembly))
                    return false;
            }
            catch (Exception ex) when (ex is FileLoadException || ex is BadImageFormatException || ex is FileNotFoundException)
            {
                if (SkipMissingAssemblies)
                {
                    Log.LogWarning(null, "WASM0004", "", "", 0, 0, 0, 0, $"Loading assembly reference '{aname}' for '{assembly.GetName()}' failed: {ex.Message} Skipping.");
                }
                else
                {
                    Log.LogError($"Failed to load assembly reference '{aname}' for '{assembly.GetName()}': {ex.Message}");
                    return false;
                }
            }
        }

        return true;
    }
}

internal sealed class SearchPathsAssemblyResolver : MetadataAssemblyResolver
{
    private readonly string[] _searchPaths;

    public SearchPathsAssemblyResolver(string[] searchPaths) => _searchPaths = searchPaths;

    public override Assembly? Resolve(MetadataLoadContext context, AssemblyName assemblyName)
    {
        string? name = assemblyName.Name;
        foreach (var dir in _searchPaths)
        {
            string path = Path.Combine(dir, name + ".dll");
            if (File.Exists(path))
                return context.LoadFromAssemblyPath(path);
        }
        return null;
    }
}
