// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.Hosting;
using DependencyContextCompilationOptions = Microsoft.Extensions.DependencyModel.CompilationOptions;

namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;

#pragma warning disable CA1852 // Seal internal types
internal class CSharpCompiler
#pragma warning restore CA1852 // Seal internal types
{
    private readonly RazorReferenceManager _referenceManager;
    private readonly IWebHostEnvironment _hostingEnvironment;
    private bool _optionsInitialized;
    private CSharpParseOptions? _parseOptions;
    private CSharpCompilationOptions? _compilationOptions;
    private EmitOptions? _emitOptions;
    private bool _emitPdb;

    public CSharpCompiler(RazorReferenceManager manager, IWebHostEnvironment hostingEnvironment)
    {
        _referenceManager = manager ?? throw new ArgumentNullException(nameof(manager));
        _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
    }

    public virtual CSharpParseOptions ParseOptions
    {
        get
        {
            EnsureOptions();
            return _parseOptions;
        }
    }

    public virtual CSharpCompilationOptions CSharpCompilationOptions
    {
        get
        {
            EnsureOptions();
            return _compilationOptions;
        }
    }

    public virtual bool EmitPdb
    {
        get
        {
            EnsureOptions();
            return _emitPdb;
        }
    }

    public virtual EmitOptions EmitOptions
    {
        get
        {
            EnsureOptions();
            return _emitOptions;
        }
    }

    public SyntaxTree CreateSyntaxTree(SourceText sourceText)
    {
        return CSharpSyntaxTree.ParseText(
            sourceText,
            options: ParseOptions);
    }

    public CSharpCompilation CreateCompilation(string assemblyName)
    {
        return CSharpCompilation.Create(
            assemblyName,
            options: CSharpCompilationOptions,
            references: _referenceManager.CompilationReferences);
    }

    // Internal for unit testing.
    protected internal virtual DependencyContextCompilationOptions GetDependencyContextCompilationOptions()
    {
        if (!string.IsNullOrEmpty(_hostingEnvironment.ApplicationName))
        {
            var applicationAssembly = Assembly.Load(new AssemblyName(_hostingEnvironment.ApplicationName));
            var dependencyContext = DependencyContext.Load(applicationAssembly);
            if (dependencyContext?.CompilationOptions != null)
            {
                return dependencyContext.CompilationOptions;
            }
        }

        return DependencyContextCompilationOptions.Default;
    }

    [MemberNotNull(nameof(_emitOptions), nameof(_parseOptions), nameof(_compilationOptions))]
    private void EnsureOptions()
    {
        if (!_optionsInitialized)
        {
            var dependencyContextOptions = GetDependencyContextCompilationOptions();
            _parseOptions = GetParseOptions(_hostingEnvironment, dependencyContextOptions);
            _compilationOptions = GetCompilationOptions(_hostingEnvironment, dependencyContextOptions);
            _emitOptions = GetEmitOptions(dependencyContextOptions);

            _optionsInitialized = true;
        }

        Debug.Assert(_parseOptions is not null);
        Debug.Assert(_compilationOptions is not null);
        Debug.Assert(_emitOptions is not null);
    }

    private EmitOptions GetEmitOptions(DependencyContextCompilationOptions dependencyContextOptions)
    {
        // Assume we're always producing pdbs unless DebugType = none
        _emitPdb = true;
        DebugInformationFormat debugInformationFormat;
        if (string.IsNullOrEmpty(dependencyContextOptions.DebugType))
        {
            debugInformationFormat = DebugInformationFormat.PortablePdb;
        }
        else
        {
            // Based on https://github.com/dotnet/roslyn/blob/1d28ff9ba248b332de3c84d23194a1d7bde07e4d/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs#L624-L640
            switch (dependencyContextOptions.DebugType.ToLowerInvariant())
            {
                case "none":
                    // There isn't a way to represent none in DebugInformationFormat.
                    // We'll set EmitPdb to false and let callers handle it by setting a null pdb-stream.
                    _emitPdb = false;
                    return new EmitOptions();
                case "portable":
                    debugInformationFormat = DebugInformationFormat.PortablePdb;
                    break;
                case "embedded":
                    // Roslyn does not expose enough public APIs to produce a binary with embedded pdbs.
                    // We'll produce PortablePdb instead to continue providing a reasonable user experience.
                    debugInformationFormat = DebugInformationFormat.PortablePdb;
                    break;
                case "full":
                case "pdbonly":
                    debugInformationFormat = DebugInformationFormat.PortablePdb;
                    break;
                default:
                    throw new InvalidOperationException(Resources.FormatUnsupportedDebugInformationFormat(dependencyContextOptions.DebugType));
            }
        }

        var emitOptions = new EmitOptions(debugInformationFormat: debugInformationFormat);
        return emitOptions;
    }

    private static CSharpCompilationOptions GetCompilationOptions(
        IWebHostEnvironment hostingEnvironment,
        DependencyContextCompilationOptions dependencyContextOptions)
    {
        var csharpCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);

        // Disable 1702 until roslyn turns this off by default
        csharpCompilationOptions = csharpCompilationOptions.WithSpecificDiagnosticOptions(
            new Dictionary<string, ReportDiagnostic>
            {
                    {"CS1701", ReportDiagnostic.Suppress}, // Binding redirects
                    {"CS1702", ReportDiagnostic.Suppress},
                    {"CS1705", ReportDiagnostic.Suppress}
            });

        if (dependencyContextOptions.AllowUnsafe.HasValue)
        {
            csharpCompilationOptions = csharpCompilationOptions.WithAllowUnsafe(
                dependencyContextOptions.AllowUnsafe.Value);
        }

        OptimizationLevel optimizationLevel;
        if (dependencyContextOptions.Optimize.HasValue)
        {
            optimizationLevel = dependencyContextOptions.Optimize.Value ?
                OptimizationLevel.Release :
                OptimizationLevel.Debug;
        }
        else
        {
            optimizationLevel = hostingEnvironment.IsDevelopment() ?
                OptimizationLevel.Debug :
                OptimizationLevel.Release;
        }
        csharpCompilationOptions = csharpCompilationOptions.WithOptimizationLevel(optimizationLevel);

        if (dependencyContextOptions.WarningsAsErrors.HasValue)
        {
            var reportDiagnostic = dependencyContextOptions.WarningsAsErrors.Value ?
                ReportDiagnostic.Error :
                ReportDiagnostic.Default;
            csharpCompilationOptions = csharpCompilationOptions.WithGeneralDiagnosticOption(reportDiagnostic);
        }

        return csharpCompilationOptions;
    }

    private static CSharpParseOptions GetParseOptions(
        IWebHostEnvironment hostingEnvironment,
        DependencyContextCompilationOptions dependencyContextOptions)
    {
        var configurationSymbol = hostingEnvironment.IsDevelopment() ? "DEBUG" : "RELEASE";
        var defines = dependencyContextOptions.Defines.Concat(new[] { configurationSymbol }).Where(define => define != null);

        var parseOptions = new CSharpParseOptions(preprocessorSymbols: (IEnumerable<string>)defines);

        if (string.IsNullOrEmpty(dependencyContextOptions.LanguageVersion))
        {
            // If the user does not specify a LanguageVersion, assume CSharp 8.0. This matches the language version Razor 3.0 targets by default.
            parseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp8);
        }
        else if (LanguageVersionFacts.TryParse(dependencyContextOptions.LanguageVersion, out var languageVersion))
        {
            parseOptions = parseOptions.WithLanguageVersion(languageVersion);
        }
        else
        {
            Debug.Fail($"LanguageVersion {languageVersion} specified in the deps file could not be parsed.");
        }

        return parseOptions;
    }
}
