// 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.ComponentModel.Composition;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.ProjectSystem;
using Roslyn.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.CPS;

[Export(typeof(ITempPECompiler))]
internal sealed class TempPECompiler : ITempPECompiler
{
    private readonly VisualStudioWorkspace _workspace;

    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public TempPECompiler(VisualStudioWorkspace workspace)
        => _workspace = workspace;

    public async Task<bool> CompileAsync(IWorkspaceProjectContext context, string outputFileName, ISet<string> filesToInclude, CancellationToken cancellationToken)
    {
        if (filesToInclude == null || filesToInclude.Count == 0)
        {
            throw new ArgumentException(nameof(filesToInclude), "Must specify some files to compile.");
        }

        if (outputFileName == null)
        {
            throw new ArgumentException(nameof(outputFileName), "Must specify an output file name.");
        }

        var project = _workspace.CurrentSolution.GetRequiredProject(context.Id);

        // Start by fetching the compilation we have already have that will have references correct
        var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);

        // Update to just the syntax trees we need to keep
        var syntaxTrees = new List<SyntaxTree>(capacity: filesToInclude.Count);
        foreach (var document in project.Documents)
        {
            if (document.FilePath != null && filesToInclude.Contains(document.FilePath))
            {
                syntaxTrees.Add(await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false));
            }

            cancellationToken.ThrowIfCancellationRequested();
        }

        compilation = compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(syntaxTrees);

        // We need to inherit most of the projects options, mainly for VB (RootNamespace, GlobalImports etc.), but we need to override about some specific things surrounding the output
        compilation = compilation.WithOptions(compilation.Options
                // copied from the old TempPE compiler used by legacy, for parity.
                // See: https://github.com/dotnet/roslyn/blob/fab7134296816fc80019c60b0f5bef7400cf23ea/src/VisualStudio/CSharp/Impl/ProjectSystemShim/TempPECompilerService.cs#L58
                .WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default)
                .WithSourceReferenceResolver(SourceFileResolver.Default)
                .WithXmlReferenceResolver(XmlFileResolver.Default)
                // We always want to produce a debug, AnyCPU DLL
                .WithOutputKind(OutputKind.DynamicallyLinkedLibrary)
                .WithPlatform(Platform.AnyCpu)
                .WithOptimizationLevel(OptimizationLevel.Debug)
                // Turn off any warnings as errors just in case
                .WithGeneralDiagnosticOption(ReportDiagnostic.Suppress)
                .WithReportSuppressedDiagnostics(false)
                .WithSpecificDiagnosticOptions(null)
                // Turn off any signing and strong naming
                .WithDelaySign(false)
                .WithCryptoKeyFile(null)
                .WithPublicSign(false)
                .WithStrongNameProvider(null));

        // AssemblyName should be set to the filename of the output file because multiple TempPE DLLs can be created for the same project
        compilation = compilation.WithAssemblyName(Path.GetFileName(outputFileName));

        cancellationToken.ThrowIfCancellationRequested();

        var outputPath = Path.GetDirectoryName(outputFileName);

        Directory.CreateDirectory(outputPath);

        using var file = FileUtilities.CreateFileStreamChecked(File.Create, outputFileName, nameof(outputFileName));
        return compilation.Emit(file, cancellationToken: cancellationToken).Success;
    }
}
