// 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 System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGen;
using Microsoft.CodeAnalysis.Rebuild;
using Microsoft.Extensions.Logging;

namespace BuildValidator
{
    /// <summary>
    /// Resolves references for a package by looking in local nuget and artifact
    /// directories for Roslyn
    /// </summary>
    internal class LocalReferenceResolver
    {
        /// <summary>
        /// This maps MVID to the <see cref="AssemblyInfo"/> we are using for that particular MVID.
        /// </summary>
        private readonly Dictionary<Guid, AssemblyInfo> _mvidMap = new();

        /// <summary>
        /// Map file names to all of the paths it exists at. This map is depopulated as we realize
        /// the information from these file locations.
        /// </summary>
        private readonly Dictionary<string, List<string>> _nameToLocationsMap = new();

        /// <summary>
        /// This maps a given file name to all of the <see cref="AssemblyInfo"/> that we ever considered 
        /// for that file name. It's useful for diagnostic purposes to see where we may have missed a
        /// reference lookup.
        /// </summary>
        private readonly Dictionary<string, List<AssemblyInfo>> _nameMap = new(FileNameEqualityComparer.StringComparer);
        private readonly HashSet<DirectoryInfo> _indexDirectories = new();
        private readonly ILogger _logger;

        private LocalReferenceResolver(Dictionary<string, List<string>> nameToLocationsMap, ILogger logger)
        {
            _nameToLocationsMap = nameToLocationsMap;
            _logger = logger;
        }

        public static LocalReferenceResolver Create(Options options, ILoggerFactory loggerFactory)
        {
            var logger = loggerFactory.CreateLogger<LocalReferenceResolver>();
            var directories = new List<DirectoryInfo>();
            foreach (var path in options.AssembliesPaths)
            {
                directories.Add(new DirectoryInfo(path));
            }

            directories.Add(GetNugetCacheDirectory());
            foreach (var path in options.ReferencesPaths)
            {
                directories.Add(new DirectoryInfo(path));
            }

            using var _ = logger.BeginScope("Assembly Location Cache Population");
            var nameToLocationsMap = new Dictionary<string, List<string>>();
            foreach (var directory in directories)
            {
                logger.LogInformation($"Searching {directory.FullName}");
                var allFiles = directory
                    .EnumerateFiles("*.dll", SearchOption.AllDirectories)
                    .Concat(directory.EnumerateFiles("*.exe", SearchOption.AllDirectories));

                foreach (var fileInfo in allFiles)
                {
                    if (!nameToLocationsMap.TryGetValue(fileInfo.Name, out var locations))
                    {
                        locations = new();
                        nameToLocationsMap[fileInfo.Name] = locations;
                    }

                    locations.Add(fileInfo.FullName);
                }
            }

            return new LocalReferenceResolver(nameToLocationsMap, logger);
        }

        public static DirectoryInfo GetNugetCacheDirectory()
        {
            var nugetPackageDirectory = Environment.GetEnvironmentVariable("NUGET_PACKAGES");
            nugetPackageDirectory ??= Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget");

            return new DirectoryInfo(nugetPackageDirectory);
        }

        public IEnumerable<AssemblyInfo> GetCachedAssemblyInfos(string fileName) => _nameMap.TryGetValue(fileName, out var list)
            ? list
            : Array.Empty<AssemblyInfo>();

        public bool TryGetCachedAssemblyInfo(Guid mvid, [NotNullWhen(true)] out AssemblyInfo? assemblyInfo) => _mvidMap.TryGetValue(mvid, out assemblyInfo);

        public string GetCachedReferencePath(MetadataReferenceInfo referenceInfo)
        {
            if (_mvidMap.TryGetValue(referenceInfo.ModuleVersionId, out var value))
            {
                return value.FilePath;
            }

            throw new Exception($"Could not find referenced assembly {referenceInfo}");
        }

        public bool TryResolveReferences(MetadataReferenceInfo metadataReferenceInfo, [NotNullWhen(true)] out MetadataReference? metadataReference)
        {
            if (!TryGetAssemblyInfo(metadataReferenceInfo, out var assemblyInfo))
            {
                metadataReference = null;
                return false;
            }

            // This is deliberately using an ordinal comparison here. The name of the assembly is written out 
            // into the PDB. Rebuild will only succeed if the provided reference has the same name with the
            // same casing
            var filePath = assemblyInfo.FilePath;
            if (Path.GetFileName(filePath) != metadataReferenceInfo.FileName)
            {
                filePath = Path.Combine(Path.GetDirectoryName(filePath)!, metadataReferenceInfo.FileName);
            }

            metadataReference = MetadataReference.CreateFromStream(
                File.OpenRead(assemblyInfo.FilePath),
                filePath: filePath,
                properties: new MetadataReferenceProperties(
                    kind: MetadataImageKind.Assembly,
                    aliases: metadataReferenceInfo.ExternAlias is null ? ImmutableArray<string>.Empty : ImmutableArray.Create(metadataReferenceInfo.ExternAlias),
                    embedInteropTypes: metadataReferenceInfo.EmbedInteropTypes));
            return true;
        }

        public bool TryGetAssemblyInfo(MetadataReferenceInfo metadataReferenceInfo, [NotNullWhen(true)] out AssemblyInfo? assemblyInfo)
        {
            EnsureCachePopulated(metadataReferenceInfo.FileName);
            return _mvidMap.TryGetValue(metadataReferenceInfo.ModuleVersionId, out assemblyInfo);
        }

        private void EnsureCachePopulated(string fileName)
        {
            if (!_nameToLocationsMap.TryGetValue(fileName, out var locations))
            {
                return;
            }

            _nameToLocationsMap.Remove(fileName);

            using var _ = _logger.BeginScope($"Populating {fileName}");
            var assemblyInfoList = new List<AssemblyInfo>();
            foreach (var filePath in locations)
            {
                if (Util.GetPortableExecutableInfo(filePath) is not { } peInfo)
                {
                    _logger.LogWarning($@"Could not read MVID from ""{filePath}""");
                    continue;
                }

                if (peInfo.IsReadyToRun)
                {
                    _logger.LogInformation($@"Skipping ReadyToRun image ""{filePath}""");
                    continue;
                }

                var currentInfo = new AssemblyInfo(filePath, peInfo.Mvid);
                assemblyInfoList.Add(currentInfo);

                if (!_mvidMap.ContainsKey(peInfo.Mvid))
                {
                    _logger.LogTrace($"Caching [{peInfo.Mvid}, {filePath}]");
                    _mvidMap[peInfo.Mvid] = currentInfo;
                }
            }

            _nameMap[fileName] = assemblyInfoList;
        }
    }
}
