﻿// 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.Immutable;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Rebuild
{
    public abstract class CompilationFactory
    {
        public string AssemblyFileName { get; }
        public CompilationOptionsReader OptionsReader { get; }
        public ParseOptions ParseOptions => CommonParseOptions;
        public CompilationOptions CompilationOptions => CommonCompilationOptions;

        protected abstract ParseOptions CommonParseOptions { get; }
        protected abstract CompilationOptions CommonCompilationOptions { get; }

        protected CompilationFactory(string assemblyFileName, CompilationOptionsReader optionsReader)
        {
            AssemblyFileName = assemblyFileName;
            OptionsReader = optionsReader;
        }

        public static CompilationFactory Create(string assemblyFileName, CompilationOptionsReader optionsReader)
            => optionsReader.GetLanguageName() switch
            {
                LanguageNames.CSharp => CSharpCompilationFactory.Create(assemblyFileName, optionsReader),
                LanguageNames.VisualBasic => VisualBasicCompilationFactory.Create(assemblyFileName, optionsReader),
                var language => throw new InvalidDataException($"{assemblyFileName} has unsupported language {language}")
            };

        public abstract SyntaxTree CreateSyntaxTree(string filePath, SourceText sourceText);

        public Compilation CreateCompilation(IRebuildArtifactResolver resolver)
        {
            var tuple = OptionsReader.ResolveArtifacts(resolver, CreateSyntaxTree);
            return CreateCompilation(tuple.SyntaxTrees, tuple.MetadataReferences);
        }

        public abstract Compilation CreateCompilation(
            ImmutableArray<SyntaxTree> syntaxTrees,
            ImmutableArray<MetadataReference> metadataReferences);

        public EmitResult Emit(
            Stream rebuildPeStream,
            Stream? rebuildPdbStream,
            IRebuildArtifactResolver rebuildArtifactResolver,
            CancellationToken cancellationToken)
            => Emit(
                rebuildPeStream,
                rebuildPdbStream,
                CreateCompilation(rebuildArtifactResolver),
                cancellationToken);

        public EmitResult Emit(
            Stream rebuildPeStream,
            Stream? rebuildPdbStream,
            ImmutableArray<SyntaxTree> syntaxTrees,
            ImmutableArray<MetadataReference> metadataReferences,
            CancellationToken cancellationToken)
            => Emit(
                rebuildPeStream,
                rebuildPdbStream,
                CreateCompilation(syntaxTrees, metadataReferences),
                cancellationToken);

        public EmitResult Emit(
            Stream rebuildPeStream,
            Stream? rebuildPdbStream,
            Compilation rebuildCompilation,
            CancellationToken cancellationToken)
        {
            var embeddedTexts = rebuildCompilation.SyntaxTrees
                    .Select(st => (path: st.FilePath, text: st.GetText()))
                    .Where(pair => pair.text.CanBeEmbedded)
                    .Select(pair => EmbeddedText.FromSource(pair.path, pair.text))
                    .ToImmutableArray();

            return Emit(
                rebuildPeStream,
                rebuildPdbStream,
                rebuildCompilation,
                embeddedTexts,
                cancellationToken);
        }

        public unsafe EmitResult Emit(
            Stream rebuildPeStream,
            Stream? rebuildPdbStream,
            Compilation rebuildCompilation,
            ImmutableArray<EmbeddedText> embeddedTexts,
            CancellationToken cancellationToken)
        {
            var peHeader = OptionsReader.PeReader.PEHeaders.PEHeader!;
            var win32Resources = OptionsReader.PeReader.GetSectionData(peHeader.ResourceTableDirectory.RelativeVirtualAddress);
            using var win32ResourceStream = win32Resources.Pointer != null
                ? new UnmanagedMemoryStream(win32Resources.Pointer, win32Resources.Length)
                : null;

            var sourceLink = OptionsReader.GetSourceLinkUtf8();

            var debugEntryPoint = getDebugEntryPoint();
            string? pdbFilePath;
            DebugInformationFormat debugInformationFormat;
            if (OptionsReader.HasEmbeddedPdb)
            {
                if (rebuildPdbStream is object)
                {
                    throw new ArgumentException(RebuildResources.PDB_stream_must_be_null_because_the_compilation_has_an_embedded_PDB, nameof(rebuildPdbStream));
                }

                debugInformationFormat = DebugInformationFormat.Embedded;
                pdbFilePath = null;
            }
            else
            {
                if (rebuildPdbStream is null)
                {
                    throw new ArgumentException(RebuildResources.A_non_null_PDB_stream_must_be_provided_because_the_compilation_does_not_have_an_embedded_PDB, nameof(rebuildPdbStream));
                }

                debugInformationFormat = DebugInformationFormat.PortablePdb;
                var codeViewEntry = OptionsReader.PeReader.ReadDebugDirectory().Single(entry => entry.Type == DebugDirectoryEntryType.CodeView);
                var codeView = OptionsReader.PeReader.ReadCodeViewDebugDirectoryData(codeViewEntry);
                pdbFilePath = codeView.Path ?? throw new InvalidOperationException(RebuildResources.Could_not_get_PDB_file_path);
            }

            var rebuildData = new RebuildData(
                OptionsReader.GetMetadataCompilationOptionsBlobReader(),
                getNonSourceFileDocumentNames(OptionsReader.PdbReader, OptionsReader.GetSourceFileCount()));
            var emitResult = rebuildCompilation.Emit(
                peStream: rebuildPeStream,
                pdbStream: rebuildPdbStream,
                xmlDocumentationStream: null,
                win32Resources: win32ResourceStream,
                manifestResources: OptionsReader.GetManifestResources(),
                options: new EmitOptions(
                    debugInformationFormat: debugInformationFormat,
                    pdbFilePath: pdbFilePath,
                    highEntropyVirtualAddressSpace: (peHeader.DllCharacteristics & DllCharacteristics.HighEntropyVirtualAddressSpace) != 0,
                    subsystemVersion: SubsystemVersion.Create(peHeader.MajorSubsystemVersion, peHeader.MinorSubsystemVersion)),
                debugEntryPoint: debugEntryPoint,
                metadataPEStream: null,
                rebuildData: rebuildData,
                sourceLinkStream: sourceLink != null ? new MemoryStream(sourceLink) : null,
                embeddedTexts: embeddedTexts,
                cancellationToken: cancellationToken);

            return emitResult;

            static ImmutableArray<string> getNonSourceFileDocumentNames(MetadataReader pdbReader, int sourceFileCount)
            {
                var count = pdbReader.Documents.Count - sourceFileCount;
                var builder = ArrayBuilder<string>.GetInstance(count);
                foreach (var documentHandle in pdbReader.Documents.Skip(sourceFileCount))
                {
                    var document = pdbReader.GetDocument(documentHandle);
                    var name = pdbReader.GetString(document.Name);
                    builder.Add(name);
                }
                return builder.ToImmutableAndFree();
            }

            IMethodSymbol? getDebugEntryPoint()
            {
                if (OptionsReader.GetMainMethodInfo() is (string mainTypeName, string mainMethodName))
                {
                    var typeSymbol = rebuildCompilation.GetTypeByMetadataName(mainTypeName);
                    if (typeSymbol is object)
                    {
                        var methodSymbols = typeSymbol
                            .GetMembers(mainMethodName)
                            .OfType<IMethodSymbol>();
                        return methodSymbols.FirstOrDefault();
                    }
                }

                return null;
            }
        }

        protected static (OptimizationLevel OptimizationLevel, bool DebugPlus) GetOptimizationLevel(string? value)
        {
            if (value is null)
            {
                return OptimizationLevelFacts.DefaultValues;
            }

            if (!OptimizationLevelFacts.TryParsePdbSerializedString(value, out OptimizationLevel optimizationLevel, out bool debugPlus))
            {
                throw new InvalidOperationException();
            }

            return (optimizationLevel, debugPlus);
        }

        protected static Platform GetPlatform(string? platform)
            => platform is null
                ? Platform.AnyCpu
                : (Platform)Enum.Parse(typeof(Platform), platform);
    }
}
