﻿// 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.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Threading;
using System.Diagnostics;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Symbols;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Represents an immutable snapshot of assembly CLI metadata.
    /// </summary>
    public sealed class AssemblyMetadata : Metadata
    {
        private sealed class Data
        {
            public static readonly Data Disposed = new Data();

            public readonly ImmutableArray<ModuleMetadata> Modules;
            public readonly PEAssembly? Assembly;

            private Data()
            {
            }

            public Data(ImmutableArray<ModuleMetadata> modules, PEAssembly assembly)
            {
                Debug.Assert(!modules.IsDefaultOrEmpty && assembly != null);

                this.Modules = modules;
                this.Assembly = assembly;
            }

            public bool IsDisposed
            {
                get { return Assembly == null; }
            }
        }

        /// <summary>
        /// Factory that provides the <see cref="ModuleMetadata"/> for additional modules (other than <see cref="_initialModules"/>) of the assembly.
        /// Shall only throw <see cref="BadImageFormatException"/> or <see cref="IOException"/>.
        /// Null of all modules were specified at construction time.
        /// </summary>
        private readonly Func<string, ModuleMetadata>? _moduleFactoryOpt;

        /// <summary>
        /// Modules the <see cref="AssemblyMetadata"/> was created with, in case they are eagerly allocated.
        /// </summary>
        private readonly ImmutableArray<ModuleMetadata> _initialModules;

        // Encapsulates the modules and the corresponding PEAssembly produced by the modules factory.
        private Data? _lazyData;

        // The actual array of modules exposed via Modules property.
        // The same modules as the ones produced by the factory or their copies.
        private ImmutableArray<ModuleMetadata> _lazyPublishedModules;

        /// <summary>
        /// Cached assembly symbols.
        /// </summary>
        /// <remarks>
        /// Guarded by <see cref="CommonReferenceManager.SymbolCacheAndReferenceManagerStateGuard"/>.
        /// </remarks>
        internal readonly WeakList<IAssemblySymbolInternal> CachedSymbols = new WeakList<IAssemblySymbolInternal>();

        // creates a copy
        private AssemblyMetadata(AssemblyMetadata other, bool shareCachedSymbols)
            : base(isImageOwner: false, id: other.Id)
        {
            if (shareCachedSymbols)
            {
                this.CachedSymbols = other.CachedSymbols;
            }

            _lazyData = other._lazyData;
            _moduleFactoryOpt = other._moduleFactoryOpt;
            _initialModules = other._initialModules;
            // Leave lazyPublishedModules unset. Published modules will be set and copied as needed.
        }

        internal AssemblyMetadata(ImmutableArray<ModuleMetadata> modules)
            : base(isImageOwner: true, id: MetadataId.CreateNewId())
        {
            Debug.Assert(!modules.IsDefaultOrEmpty);
            _initialModules = modules;
        }

        internal AssemblyMetadata(ModuleMetadata manifestModule, Func<string, ModuleMetadata> moduleFactory)
            : base(isImageOwner: true, id: MetadataId.CreateNewId())
        {
            Debug.Assert(manifestModule != null);
            Debug.Assert(moduleFactory != null);

            _initialModules = ImmutableArray.Create(manifestModule);
            _moduleFactoryOpt = moduleFactory;
        }

        /// <summary>
        /// Creates a single-module assembly.
        /// </summary>
        /// <param name="peImage">
        /// Manifest module image.
        /// </param>
        /// <exception cref="ArgumentNullException"><paramref name="peImage"/> is null.</exception>
        public static AssemblyMetadata CreateFromImage(ImmutableArray<byte> peImage)
        {
            return Create(ModuleMetadata.CreateFromImage(peImage));
        }

        /// <summary>
        /// Creates a single-module assembly.
        /// </summary>
        /// <param name="peImage">
        /// Manifest module image.
        /// </param>
        /// <exception cref="ArgumentNullException"><paramref name="peImage"/> is null.</exception>
        /// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
        public static AssemblyMetadata CreateFromImage(IEnumerable<byte> peImage)
        {
            return Create(ModuleMetadata.CreateFromImage(peImage));
        }

        /// <summary>
        /// Creates a single-module assembly.
        /// </summary>
        /// <param name="peStream">Manifest module PE image stream.</param>
        /// <param name="leaveOpen">False to close the stream upon disposal of the metadata.</param>
        /// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
        public static AssemblyMetadata CreateFromStream(Stream peStream, bool leaveOpen = false)
        {
            return Create(ModuleMetadata.CreateFromStream(peStream, leaveOpen));
        }

        /// <summary>
        /// Creates a single-module assembly.
        /// </summary>
        /// <param name="peStream">Manifest module PE image stream.</param>
        /// <param name="options">False to close the stream upon disposal of the metadata.</param>
        /// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
        public static AssemblyMetadata CreateFromStream(Stream peStream, PEStreamOptions options)
        {
            return Create(ModuleMetadata.CreateFromStream(peStream, options));
        }

        /// <summary>
        /// Finds all modules of an assembly on a specified path and builds an instance of <see cref="AssemblyMetadata"/> that represents them.
        /// </summary>
        /// <param name="path">The full path to the assembly on disk.</param>
        /// <exception cref="ArgumentNullException"><paramref name="path"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="path"/> is invalid.</exception>
        /// <exception cref="IOException">Error reading file <paramref name="path"/>. See <see cref="Exception.InnerException"/> for details.</exception>
        /// <exception cref="NotSupportedException">Reading from a file path is not supported by the platform.</exception>
        public static AssemblyMetadata CreateFromFile(string path)
        {
            return CreateFromFile(ModuleMetadata.CreateFromFile(path), path);
        }

        internal static AssemblyMetadata CreateFromFile(ModuleMetadata manifestModule, string path)
        {
            return new AssemblyMetadata(manifestModule, moduleName => ModuleMetadata.CreateFromFile(Path.Combine(Path.GetDirectoryName(path) ?? "", moduleName)));
        }

        /// <summary>
        /// Creates a single-module assembly.
        /// </summary>
        /// <param name="module">
        /// Manifest module.
        /// </param>
        /// <remarks>This object disposes <paramref name="module"/> it when it is itself disposed.</remarks>
        public static AssemblyMetadata Create(ModuleMetadata module)
        {
            if (module == null)
            {
                throw new ArgumentNullException(nameof(module));
            }

            return new AssemblyMetadata(ImmutableArray.Create(module));
        }

        /// <summary>
        /// Creates a multi-module assembly.
        /// </summary>
        /// <param name="modules">
        /// Modules comprising the assembly. The first module is the manifest module of the assembly.</param>
        /// <remarks>This object disposes the elements of <paramref name="modules"/> it when it is itself <see cref="Dispose"/>.</remarks>
        /// <exception cref="ArgumentException"><paramref name="modules"/> is default value.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="modules"/> contains null elements.</exception>
        /// <exception cref="ArgumentException"><paramref name="modules"/> is empty or contains a module that doesn't own its image (was created via <see cref="Metadata.Copy"/>).</exception>
        public static AssemblyMetadata Create(ImmutableArray<ModuleMetadata> modules)
        {
            if (modules.IsDefaultOrEmpty)
            {
                throw new ArgumentException(CodeAnalysisResources.AssemblyMustHaveAtLeastOneModule, nameof(modules));
            }

            for (int i = 0; i < modules.Length; i++)
            {
                if (modules[i] == null)
                {
                    throw new ArgumentNullException(nameof(modules) + "[" + i + "]");
                }

                if (!modules[i].IsImageOwner)
                {
                    throw new ArgumentException(CodeAnalysisResources.ModuleCopyCannotBeUsedToCreateAssemblyMetadata, nameof(modules) + "[" + i + "]");
                }
            }

            return new AssemblyMetadata(modules);
        }

        /// <summary>
        /// Creates a multi-module assembly.
        /// </summary>
        /// <param name="modules">
        /// Modules comprising the assembly. The first module is the manifest module of the assembly.</param>
        /// <remarks>This object disposes the elements of <paramref name="modules"/> it when it is itself <see cref="Dispose"/>.</remarks>
        /// <exception cref="ArgumentException"><paramref name="modules"/> is default value.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="modules"/> contains null elements.</exception>
        /// <exception cref="ArgumentException"><paramref name="modules"/> is empty or contains a module that doesn't own its image (was created via <see cref="Metadata.Copy"/>).</exception>
        public static AssemblyMetadata Create(IEnumerable<ModuleMetadata> modules)
        {
            return Create(modules.AsImmutableOrNull());
        }

        /// <summary>
        /// Creates a multi-module assembly.
        /// </summary>
        /// <param name="modules">Modules comprising the assembly. The first module is the manifest module of the assembly.</param>
        /// <remarks>This object disposes the elements of <paramref name="modules"/> it when it is itself <see cref="Dispose"/>.</remarks>
        /// <exception cref="ArgumentException"><paramref name="modules"/> is default value.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="modules"/> contains null elements.</exception>
        /// <exception cref="ArgumentException"><paramref name="modules"/> is empty or contains a module that doesn't own its image (was created via <see cref="Metadata.Copy"/>).</exception>
        public static AssemblyMetadata Create(params ModuleMetadata[] modules)
        {
            return Create(ImmutableArray.CreateRange(modules));
        }

        /// <summary>
        /// Creates a shallow copy of contained modules and wraps them into a new instance of <see cref="AssemblyMetadata"/>.
        /// </summary>
        /// <remarks>
        /// The resulting copy shares the metadata images and metadata information read from them with the original.
        /// It doesn't own the underlying metadata images and is not responsible for its disposal.
        /// 
        /// This is used, for example, when a metadata cache needs to return the cached metadata to its users
        /// while keeping the ownership of the cached metadata object.
        /// </remarks>
        internal new AssemblyMetadata Copy()
        {
            return new AssemblyMetadata(this, shareCachedSymbols: true);
        }

        internal AssemblyMetadata CopyWithoutSharingCachedSymbols()
        {
            return new AssemblyMetadata(this, shareCachedSymbols: false);
        }

        protected override Metadata CommonCopy()
        {
            return Copy();
        }

        /// <summary>
        /// Modules comprising this assembly. The first module is the manifest module.
        /// </summary>
        /// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
        /// <exception cref="IOException">IO error reading the metadata. See <see cref="Exception.InnerException"/> for details.</exception>
        /// <exception cref="ObjectDisposedException">The object has been disposed.</exception>
        public ImmutableArray<ModuleMetadata> GetModules()
        {
            if (_lazyPublishedModules.IsDefault)
            {
                var data = GetOrCreateData();
                var newModules = data.Modules;

                if (!IsImageOwner)
                {
                    newModules = newModules.SelectAsArray(module => module.Copy());
                }

                ImmutableInterlocked.InterlockedInitialize(ref _lazyPublishedModules, newModules);
            }

            if (_lazyData == Data.Disposed)
            {
                throw new ObjectDisposedException(nameof(AssemblyMetadata));
            }

            return _lazyPublishedModules;
        }

        /// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
        /// <exception cref="IOException">IO error while reading the metadata. See <see cref="Exception.InnerException"/> for details.</exception>
        /// <exception cref="ObjectDisposedException">The object has been disposed.</exception>
        internal PEAssembly? GetAssembly()
        {
            return GetOrCreateData().Assembly;
        }

        /// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
        /// <exception cref="IOException">IO error while reading the metadata. See <see cref="Exception.InnerException"/> for details.</exception>
        /// <exception cref="ObjectDisposedException">The object has been disposed.</exception>
        private Data GetOrCreateData()
        {
            if (_lazyData == null)
            {
                ImmutableArray<ModuleMetadata> modules = _initialModules;
                ImmutableArray<ModuleMetadata>.Builder? moduleBuilder = null;

                bool createdModulesUsed = false;
                try
                {
                    if (_moduleFactoryOpt != null)
                    {
                        Debug.Assert(_initialModules.Length == 1);

                        var additionalModuleNames = _initialModules[0].GetModuleNames();
                        if (additionalModuleNames.Length > 0)
                        {
                            moduleBuilder = ImmutableArray.CreateBuilder<ModuleMetadata>(1 + additionalModuleNames.Length);
                            moduleBuilder.Add(_initialModules[0]);

                            foreach (string moduleName in additionalModuleNames)
                            {
                                moduleBuilder.Add(_moduleFactoryOpt(moduleName));
                            }

                            modules = moduleBuilder.ToImmutable();
                        }
                    }

                    var assembly = new PEAssembly(this, modules.SelectAsArray(m => m.Module));
                    var newData = new Data(modules, assembly);

                    createdModulesUsed = Interlocked.CompareExchange(ref _lazyData, newData, null) == null;
                }
                finally
                {
                    if (moduleBuilder != null && !createdModulesUsed)
                    {
                        // dispose unused modules created above:
                        for (int i = _initialModules.Length; i < moduleBuilder.Count; i++)
                        {
                            moduleBuilder[i].Dispose();
                        }
                    }
                }
            }

            if (_lazyData.IsDisposed)
            {
                throw new ObjectDisposedException(nameof(AssemblyMetadata));
            }

            return _lazyData;
        }

        /// <summary>
        /// Disposes all modules contained in the assembly.
        /// </summary>
        public override void Dispose()
        {
            var previousData = Interlocked.Exchange(ref _lazyData, Data.Disposed);

            if (previousData == Data.Disposed || !this.IsImageOwner)
            {
                // already disposed, or not an owner
                return;
            }

            // AssemblyMetadata assumes their ownership of all modules passed to the constructor.
            foreach (var module in _initialModules)
            {
                module.Dispose();
            }

            if (previousData == null)
            {
                // no additional modules were created yet => nothing to dispose
                return;
            }

            Debug.Assert(_initialModules.Length == 1 || _initialModules.Length == previousData.Modules.Length);

            // dispose additional modules created lazily:
            for (int i = _initialModules.Length; i < previousData.Modules.Length; i++)
            {
                previousData.Modules[i].Dispose();
            }
        }

        /// <summary>
        /// Checks if the first module has a single row in Assembly table and that all other modules have none.
        /// </summary>
        /// <exception cref="BadImageFormatException">The PE image format is invalid.</exception>
        /// <exception cref="IOException">IO error reading the metadata. See <see cref="Exception.InnerException"/> for details.</exception>
        /// <exception cref="ObjectDisposedException">The object has been disposed.</exception>
        internal bool IsValidAssembly()
        {
            var modules = GetModules();

            if (!modules[0].Module.IsManifestModule)
            {
                return false;
            }

            for (int i = 1; i < modules.Length; i++)
            {
                // Ignore winmd modules since runtime winmd modules may be loaded as non-primary modules.
                var module = modules[i].Module;
                if (!module.IsLinkedModule && module.MetadataReader.MetadataKind != MetadataKind.WindowsMetadata)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Returns the metadata kind. <see cref="MetadataImageKind"/>
        /// </summary>
        public override MetadataImageKind Kind
        {
            get { return MetadataImageKind.Assembly; }
        }

        /// <summary>
        /// Creates a reference to the assembly metadata.
        /// </summary>
        /// <param name="documentation">Provider of XML documentation comments for the metadata symbols contained in the module.</param>
        /// <param name="aliases">Aliases that can be used to refer to the assembly from source code (see "extern alias" directive in C#).</param>
        /// <param name="embedInteropTypes">True to embed interop types from the referenced assembly to the referencing compilation. Must be false for a module.</param>
        /// <param name="filePath">Path describing the location of the metadata, or null if the metadata have no location.</param>
        /// <param name="display">Display string used in error messages to identity the reference.</param>
        /// <returns>A reference to the assembly metadata.</returns>
        public PortableExecutableReference GetReference(
            DocumentationProvider? documentation = null,
            ImmutableArray<string> aliases = default,
            bool embedInteropTypes = false,
            string? filePath = null,
            string? display = null)
        {
            return new MetadataImageReference(this, new MetadataReferenceProperties(MetadataImageKind.Assembly, aliases, embedInteropTypes), documentation, filePath, display);
        }
    }
}
