// 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;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.DiaSymReader;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ExpressionEvaluator
{
    internal static partial class MetadataUtilities
    {
        /// <summary>
        /// Group module metadata into assemblies.
        /// If <paramref name="moduleId"/> is set, the
        /// assemblies are limited to those referenced by that module.
        /// </summary>
        internal static ImmutableArray<MetadataReference> MakeAssemblyReferences(
            this ImmutableArray<MetadataBlock> metadataBlocks,
            ModuleId moduleId,
            AssemblyIdentityComparer identityComparer,
            MakeAssemblyReferencesKind kind,
            out IReadOnlyDictionary<string, ImmutableArray<(AssemblyIdentity, MetadataReference)>>? referencesBySimpleName)
        {
            RoslynDebug.Assert(kind == MakeAssemblyReferencesKind.AllAssemblies || moduleId.Id != Guid.Empty);
            RoslynDebug.Assert(moduleId.Id == Guid.Empty || identityComparer != null);

            // Get metadata for each module.
            var metadataBuilder = ArrayBuilder<ModuleMetadata>.GetInstance();
            // Win8 applications contain a reference to Windows.winmd version >= 1.3
            // and perhaps multiple application winmds. At runtime, Windows.winmd
            // is replaced by multiple Windows.*.winmd version >= 1.3. In the EE, we
            // need to map compile-time assembly references to the runtime assemblies
            // supplied by the debugger. To do so, we "merge" all winmds named
            // Windows.*.winmd into a single fake Windows.winmd at runtime.
            // All other (application) winmds are left as is.
            var runtimeWinMdBuilder = ArrayBuilder<ModuleMetadata>.GetInstance();
            AssemblyIdentity? corLibrary = null;
            foreach (var block in metadataBlocks)
            {
                var metadata = ModuleMetadata.CreateFromMetadata(block.Pointer, block.Size, includeEmbeddedInteropTypes: true);
                try
                {
                    var reader = metadata.MetadataReader;
                    if (corLibrary == null)
                    {
                        bool hasNoAssemblyRefs = reader.AssemblyReferences.Count == 0;
                        // .NET Native uses a corlib with references
                        // (see https://github.com/dotnet/roslyn/issues/13275).
                        if (hasNoAssemblyRefs || metadata.Name.Equals("System.Private.CoreLib.dll", StringComparison.OrdinalIgnoreCase))
                        {
                            // If this assembly declares System.Object, assume it is the corlib.
                            // (Note, it is order dependent which assembly we treat as corlib
                            // if there are multiple assemblies that meet these requirements.
                            // That should be acceptable for evaluating expressions in the EE though.) 
                            if (reader.DeclaresTheObjectClass())
                            {
                                corLibrary = reader.ReadAssemblyIdentityOrThrow();
                                // Compiler layer requires corlib to have no AssemblyRefs.
                                if (!hasNoAssemblyRefs)
                                {
                                    metadata = ModuleMetadata.CreateFromMetadata(block.Pointer, block.Size, includeEmbeddedInteropTypes: true, ignoreAssemblyRefs: true);
                                }
                            }
                        }
                    }
                    if (IsWindowsComponent(reader, metadata.Name))
                    {
                        runtimeWinMdBuilder.Add(metadata);
                    }
                    else
                    {
                        metadataBuilder.Add(metadata);
                    }
                }
                catch (BadImageFormatException)
                {
                    // Ignore modules with "bad" metadata.
                }
            }

            // Index non-primary netmodules by name. Multiple modules may
            // have the same name but we do not have a way to differentiate
            // netmodules other than by name so if there are duplicates, the
            // dictionary value is set to null and references are dropped when
            // generating the containing assembly metadata.
            Dictionary<string, ModuleMetadata?>? modulesByName = null;
            foreach (var metadata in metadataBuilder)
            {
                if (IsPrimaryModule(metadata))
                {
                    // Primary module. No need to add to dictionary.
                    continue;
                }

                modulesByName ??= []; // Requires case-insensitive comparison?
                var name = metadata.Name;
                modulesByName[name] = modulesByName.ContainsKey(name) ? null : metadata;
            }

            // We don't walk the references of winmd assemblies currently. (That would require walking
            // references from the current module and also the winmd assemblies.) So if there are any
            // winmd assemblies, we'll use all assemblies. See https://github.com/dotnet/roslyn/issues/26157.
            if (kind == MakeAssemblyReferencesKind.AllReferences && runtimeWinMdBuilder.Any())
            {
                kind = MakeAssemblyReferencesKind.AllAssemblies;
            }

            // Build assembly references from modules in primary module manifests.
            ImmutableArray<MetadataReference> references;

            if (kind == MakeAssemblyReferencesKind.AllReferences)
            {
                var refsBySimpleName = new Dictionary<string, ImmutableArray<(AssemblyIdentity, MetadataReference)>>(StringComparer.OrdinalIgnoreCase);
                MetadataReference? targetReference = null;

                foreach (var metadata in metadataBuilder)
                {
                    if (!IsPrimaryModule(metadata))
                    {
                        continue;
                    }

                    var reference = MakeAssemblyReference(metadata, modulesByName);
                    var reader = metadata.MetadataReader;
                    var identity = reader.ReadAssemblyIdentityOrThrow();

                    if (!refsBySimpleName.TryGetValue(identity.Name, out var refs))
                    {
                        refs = ImmutableArray<(AssemblyIdentity, MetadataReference)>.Empty;
                    }

                    refsBySimpleName[identity.Name] = refs.Add((identity, reference));

                    if (targetReference == null &&
                        reader.GetModuleVersionIdOrThrow() == moduleId.Id)
                    {
                        targetReference = reference;
                    }
                }

                var referencesBuilder = ArrayBuilder<MetadataReference>.GetInstance();

                // CommonReferenceManager<TCompilation, TAssemblySymbol>.Bind()
                // expects COR library to be included in the explicit assemblies.
                RoslynDebug.AssertNotNull(corLibrary);

                if (corLibrary != null && refsBySimpleName.TryGetValue(corLibrary.Name, out var corLibraryReferences))
                {
                    referencesBuilder.Add(corLibraryReferences[0].Item2);
                }

                RoslynDebug.AssertNotNull(targetReference);
                if (targetReference != null)
                {
                    referencesBuilder.Add(targetReference);
                }

                references = referencesBuilder.ToImmutableAndFree();
                referencesBySimpleName = refsBySimpleName;
            }
            else
            {
                var referencesBuilder = ArrayBuilder<MetadataReference>.GetInstance();
                var identitiesBuilder = (kind == MakeAssemblyReferencesKind.DirectReferencesOnly) ? ArrayBuilder<AssemblyIdentity>.GetInstance() : null;
                ModuleMetadata? targetModule = null;
                AssemblyIdentity? intrinsicsAssembly = null;

                foreach (var metadata in metadataBuilder)
                {
                    if (!IsPrimaryModule(metadata))
                    {
                        continue;
                    }

                    var reference = MakeAssemblyReference(metadata, modulesByName);
                    referencesBuilder.Add(reference);

                    if (identitiesBuilder != null)
                    {
                        var reader = metadata.MetadataReader;
                        var identity = reader.ReadAssemblyIdentityOrThrow();
                        identitiesBuilder.Add(identity);

                        if (targetModule == null &&
                            reader.GetModuleVersionIdOrThrow() == moduleId.Id)
                        {
                            targetModule = metadata;
                        }

                        if (intrinsicsAssembly == null &&
                            reader.DeclaresType((r, t) => r.IsPublicNonInterfaceType(t, ExpressionCompilerConstants.IntrinsicAssemblyNamespace, ExpressionCompilerConstants.IntrinsicAssemblyTypeName)))
                        {
                            intrinsicsAssembly = identity;
                        }
                    }
                }

                if (identitiesBuilder != null)
                {
                    // Remove assemblies not directly referenced by the target module.
                    if (targetModule != null)
                    {
                        var referencedModules = ArrayBuilder<AssemblyIdentity>.GetInstance();
                        referencedModules.Add(targetModule.MetadataReader.ReadAssemblyIdentityOrThrow());
                        referencedModules.AddRange(targetModule.MetadataReader.GetReferencedAssembliesOrThrow());
                        // Ensure COR library is included, otherwise any compilation will fail.
                        // (Note, an equivalent assembly may have already been included from
                        // GetReferencedAssembliesOrThrow above but RemoveUnreferencedModules
                        // allows duplicates.)
                        Debug.Assert(corLibrary != null);
                        if (corLibrary != null)
                        {
                            referencedModules.Add(corLibrary);
                        }
                        // Ensure Debugger intrinsic methods assembly is included.
                        if (intrinsicsAssembly != null)
                        {
                            referencedModules.Add(intrinsicsAssembly);
                        }
                        RemoveUnreferencedModules(referencesBuilder, identitiesBuilder, identityComparer, referencedModules);
                        referencedModules.Free();
                    }
                    identitiesBuilder.Free();
                }

                // Any runtime winmd modules were separated out initially. Now add
                // those to a placeholder for the missing compile time module since
                // each of the runtime winmds refer to the compile time module.
                if (runtimeWinMdBuilder.Any())
                {
                    referencesBuilder.Add(MakeCompileTimeWinMdAssemblyMetadata(runtimeWinMdBuilder));
                }

                references = referencesBuilder.ToImmutableAndFree();
                referencesBySimpleName = null;
            }

            metadataBuilder.Free();
            runtimeWinMdBuilder.Free();
            return references;
        }

        /// <summary>
        /// Remove any modules that are not in the set of referenced modules.
        /// If there are duplicates of referenced modules, potentially differing by
        /// version, one instance of the highest version is kept and others dropped.
        /// </summary>
        /// <remarks>
        /// Binding against this reduced set of modules will not handle certain valid cases
        /// where binding to full set would succeed (e.g.: binding to types outside the
        /// referenced modules). And since duplicates are dropped, this will prevent resolving
        /// ambiguities between two versions of the same assembly by using aliases. Also,
        /// there is no attempt here to follow binding redirects or to use the CLR to determine
        /// which version of an assembly to prefer when there are duplicate assemblies.
        /// </remarks>
        private static void RemoveUnreferencedModules(
            ArrayBuilder<MetadataReference> modules,
            ArrayBuilder<AssemblyIdentity> identities,
            AssemblyIdentityComparer identityComparer,
            ArrayBuilder<AssemblyIdentity> referencedModules)
        {
            Debug.Assert(modules.Count == identities.Count);

            var referencedIndices = PooledHashSet<int>.GetInstance();

            // O(n*m) where n = all assemblies and m = referenced assemblies.
            // Can this be more efficient?
            int n = identities.Count;
            int index;
            foreach (var referencedModule in referencedModules)
            {
                index = -1;
                for (int i = 0; i < n; i++)
                {
                    var identity = identities[i];
                    var compareResult = identityComparer.Compare(referencedModule, identity);
                    switch (compareResult)
                    {
                        case AssemblyIdentityComparer.ComparisonResult.NotEquivalent:
                            break;

                        case AssemblyIdentityComparer.ComparisonResult.Equivalent:
                        case AssemblyIdentityComparer.ComparisonResult.EquivalentIgnoringVersion:
                            if (index < 0 || identity.Version > identities[index].Version)
                            {
                                index = i;
                            }
                            break;

                        default:
                            throw ExceptionUtilities.UnexpectedValue(compareResult);
                    }
                }

                if (index >= 0)
                {
                    referencedIndices.Add(index);
                }
            }

            Debug.Assert(referencedIndices.Count <= modules.Count);
            Debug.Assert(referencedIndices.Count <= referencedModules.Count);

            index = 0;
            for (int i = 0; i < n; i++)
            {
                if (referencedIndices.Contains(i))
                {
                    modules[index] = modules[i];
                    index++;
                }
            }
            modules.Clip(index);

            referencedIndices.Free();
        }

        private static PortableExecutableReference MakeAssemblyReference(ModuleMetadata metadata, Dictionary<string, ModuleMetadata?>? modulesByName)
        {
            Debug.Assert(metadata.Module.IsManifestModule);

            var builder = ArrayBuilder<ModuleMetadata>.GetInstance();
            builder.Add(metadata);

            // Include any associated netmodules from the manifest.
            if (modulesByName != null)
            {
                try
                {
                    var reader = metadata.MetadataReader;
                    foreach (var handle in reader.AssemblyFiles)
                    {
                        var assemblyFile = reader.GetAssemblyFile(handle);
                        if (assemblyFile.ContainsMetadata)
                        {
                            var name = reader.GetString(assemblyFile.Name);

                            // Find the assembly file in the set of netmodules with that name.
                            // The file may be missing if the file is not a module (say a resource)
                            // or if the module has not been loaded yet. The value will be null
                            // if the name was ambiguous.
                            if (!modulesByName.TryGetValue(name, out var module))
                            {
                                // AssemblyFile names may contain file information (".dll", etc).
                                modulesByName.TryGetValue(GetFileNameWithoutExtension(name), out module);
                            }

                            if (module != null)
                            {
                                builder.Add(module);
                            }
                        }
                    }
                }
                catch (BadImageFormatException)
                {
                    // Ignore modules with "bad" metadata.
                }
            }

            var assemblyMetadata = AssemblyMetadata.Create(builder.ToImmutableAndFree());
            return assemblyMetadata.GetReference(embedInteropTypes: false, display: metadata.Name);
        }

        internal static string GetFileNameWithoutExtension(string fileName)
        {
            var lastDotIndex = fileName.LastIndexOf('.');
            var extensionStartIndex = lastDotIndex + 1;
            if ((lastDotIndex > 0) && (extensionStartIndex < fileName.Length))
            {
                var extension = fileName.Substring(extensionStartIndex);
                switch (extension)
                {
                    case "dll":
                    case "exe":
                    case "netmodule":
                    case "winmd":
                        return fileName.Substring(0, lastDotIndex);
                }
            }
            return fileName;
        }

        private static byte[] GetWindowsProxyBytes()
        {
            var assembly = typeof(ExpressionCompiler).GetTypeInfo().Assembly;
            using (var stream = assembly.GetManifestResourceStream("Microsoft.CodeAnalysis.ExpressionEvaluator.Resources.WindowsProxy.winmd"))
            {
                var bytes = new byte[stream.Length];
                using (var memoryStream = new MemoryStream(bytes))
                {
                    stream.CopyTo(memoryStream);
                }
                return bytes;
            }
        }

        private static PortableExecutableReference MakeCompileTimeWinMdAssemblyMetadata(ArrayBuilder<ModuleMetadata> runtimeModules)
        {
            var metadata = ModuleMetadata.CreateFromImage(GetWindowsProxyBytes());
            var builder = ArrayBuilder<ModuleMetadata>.GetInstance();
            builder.Add(metadata);
            builder.AddRange(runtimeModules);
            var assemblyMetadata = AssemblyMetadata.Create(builder.ToImmutableAndFree());
            return assemblyMetadata.GetReference(embedInteropTypes: false, display: metadata.Name);
        }

        private static bool IsPrimaryModule(ModuleMetadata metadata)
        {
            return metadata.Module.IsManifestModule;
        }

        internal static bool IsWindowsComponent(MetadataReader reader, string moduleName)
        {
            if (reader.MetadataKind != MetadataKind.WindowsMetadata)
            {
                return false;
            }
            if (!IsWindowsComponentName(moduleName))
            {
                return false;
            }
            int majorVersion;
            int minorVersion;
            reader.GetWinMdVersion(out majorVersion, out minorVersion);
            return (majorVersion == 1) && (minorVersion >= 3);
        }

        private static bool IsWindowsComponentName(string moduleName)
        {
            return moduleName.StartsWith("windows.", StringComparison.OrdinalIgnoreCase) &&
                moduleName.EndsWith(".winmd", StringComparison.OrdinalIgnoreCase) &&
                !moduleName.Equals("windows.winmd", StringComparison.OrdinalIgnoreCase);
        }

        internal static bool IsWindowsAssemblyName(string assemblyName)
        {
            return assemblyName.Equals("windows", StringComparison.OrdinalIgnoreCase);
        }

        internal static bool IsWindowsAssemblyIdentity(this AssemblyIdentity assemblyIdentity)
        {
            return IsWindowsAssemblyName(assemblyIdentity.Name) &&
                assemblyIdentity.ContentType == System.Reflection.AssemblyContentType.WindowsRuntime;
        }

        internal static ImmutableArray<string> GetLocalNames(this ArrayBuilder<ISymUnmanagedScope> scopes)
        {
            var builder = ArrayBuilder<string>.GetInstance();
            foreach (var scope in scopes)
            {
                foreach (var local in scope.GetLocals())
                {
                    int attributes;
                    local.GetAttributes(out attributes);
                    if (attributes == (int)LocalVariableAttributes.DebuggerHidden)
                    {
                        continue;
                    }

                    builder.SetItem(local.GetSlot(), local.GetName());
                }
            }
            return builder.ToImmutableAndFree();
        }

        internal static ImmutableArray<int> GetSynthesizedMethods(byte[] assembly, string methodName)
        {
            var builder = ArrayBuilder<int>.GetInstance();
            using (var metadata = ModuleMetadata.CreateFromStream(new MemoryStream(assembly)))
            {
                var reader = metadata.MetadataReader;
                foreach (var handle in reader.MethodDefinitions)
                {
                    var methodDef = reader.GetMethodDefinition(handle);
                    if (reader.StringComparer.Equals(methodDef.Name, methodName))
                    {
                        builder.Add(reader.GetToken(handle));
                    }
                }
            }
            return builder.ToImmutableAndFree();
        }
    }
}
