﻿// 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.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// The string returned from this function represents the inputs to the compiler which impact determinism.  It is 
    /// meant to be inline with the specification here:
    /// 
    ///     - https://github.com/dotnet/roslyn/blob/main/docs/compilers/Deterministic%20Inputs.md
    /// 
    /// Options which can cause compilation failure, but doesn't impact the result of a successful
    /// compilation should be included. That is because it is interesting to describe error states
    /// not just success states. Think about caching build failures as well as build successes.
    ///
    /// When an option is omitted, say if there is no value for a public crypto key, we should emit
    /// the property with a null value vs. omitting the property. Either approach would produce 
    /// correct results the preference is to be declarative that an option is omitted.
    /// </summary>
    internal abstract class DeterministicKeyBuilder
    {
        protected DeterministicKeyBuilder()
        {
        }

        protected void WriteFilePath(
            JsonWriter writer,
            string propertyName,
            string? filePath,
            ImmutableArray<KeyValuePair<string, string>> pathMap,
            DeterministicKeyOptions options)
        {
            if ((options & DeterministicKeyOptions.IgnorePaths) != 0)
            {
                filePath = Path.GetFileName(filePath);
            }
            else if (filePath is not null)
            {
                filePath = PathUtilities.NormalizePathPrefix(filePath, pathMap);
            }

            writer.Write(propertyName, filePath);
        }

        internal static string EncodeByteArrayValue(ReadOnlySpan<byte> value)
        {
            var builder = PooledStringBuilder.GetInstance();
            EncodeByteArrayValue(value, builder.Builder);
            return builder.ToStringAndFree();
        }

        internal static void EncodeByteArrayValue(ReadOnlySpan<byte> value, StringBuilder builder)
        {
            foreach (var b in value)
            {
                builder.Append(b.ToString("x"));
            }
        }

        protected static void WriteByteArrayValue(JsonWriter writer, string name, ReadOnlySpan<byte> value) =>
            writer.Write(name, EncodeByteArrayValue(value));

        protected static void WriteVersion(JsonWriter writer, string key, Version version)
        {
            writer.WriteKey(key);
            writer.WriteObjectStart();
            writer.Write("major", version.Major);
            writer.Write("minor", version.Minor);
            writer.Write("build", version.Build);
            writer.Write("revision", version.Revision);
            writer.WriteObjectEnd();
        }

        protected void WriteType(JsonWriter writer, string key, Type? type)
        {
            writer.WriteKey(key);
            WriteType(writer, type);
        }

        protected void WriteType(JsonWriter writer, Type? type)
        {
            if (type is null)
            {
                writer.WriteNull();
                return;
            }

            writer.WriteObjectStart();
            writer.Write("fullName", type.FullName);
            // Note that the file path to the assembly is deliberately not included here. The file path
            // of the assembly does not contribute to the output of the program.
            writer.Write("assemblyName", type.Assembly.FullName);
            writer.Write("mvid", GetGuidValue(type.Assembly.ManifestModule.ModuleVersionId));
            writer.WriteObjectEnd();
        }

        private (JsonWriter, PooledStringBuilder) CreateWriter()
        {
            var builder = PooledStringBuilder.GetInstance();
            var writer = new StringWriter(builder);
            return (new JsonWriter(writer), builder);
        }

        internal string GetKey(
            CompilationOptions compilationOptions,
            ImmutableArray<SyntaxTreeKey> syntaxTrees,
            ImmutableArray<MetadataReference> references,
            ImmutableArray<byte> publicKey,
            ImmutableArray<AdditionalText> additionalTexts,
            ImmutableArray<DiagnosticAnalyzer> analyzers,
            ImmutableArray<ISourceGenerator> generators,
            ImmutableArray<KeyValuePair<string, string>> pathMap,
            EmitOptions? emitOptions,
            DeterministicKeyOptions options,
            CancellationToken cancellationToken)
        {
            additionalTexts = additionalTexts.NullToEmpty();
            analyzers = analyzers.NullToEmpty();
            generators = generators.NullToEmpty();

            var (writer, builder) = CreateWriter();

            writer.WriteObjectStart();

            writer.WriteKey("compilation");
            WriteCompilation(writer, compilationOptions, syntaxTrees, references, publicKey, pathMap, options, cancellationToken);
            writer.WriteKey("additionalTexts");
            writeAdditionalTexts();
            writer.WriteKey("analyzers");
            writeAnalyzers();
            writer.WriteKey("generators");
            writeGenerators();
            writer.WriteKey("emitOptions");
            WriteEmitOptions(writer, emitOptions, pathMap, options);

            writer.WriteObjectEnd();

            return builder.ToStringAndFree();

            void writeAdditionalTexts()
            {
                writer.WriteArrayStart();
                foreach (var additionalText in additionalTexts)
                {
                    cancellationToken.ThrowIfCancellationRequested();

                    writer.WriteObjectStart();
                    WriteFilePath(writer, "fileName", additionalText.Path, pathMap, options);
                    writer.WriteKey("text");
                    WriteSourceText(writer, additionalText.GetText(cancellationToken));
                    writer.WriteObjectEnd();
                }
                writer.WriteArrayEnd();
            }

            void writeAnalyzers()
            {
                writer.WriteArrayStart();
                foreach (var analyzer in analyzers)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    WriteType(writer, analyzer.GetType());
                }
                writer.WriteArrayEnd();
            }

            void writeGenerators()
            {
                writer.WriteArrayStart();
                foreach (var generator in generators)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    WriteType(writer, generator.GetType());
                }
                writer.WriteArrayEnd();
            }
        }

        internal static string GetGuidValue(in Guid guid) => guid.ToString("D");

        private void WriteCompilation(
            JsonWriter writer,
            CompilationOptions compilationOptions,
            ImmutableArray<SyntaxTreeKey> syntaxTrees,
            ImmutableArray<MetadataReference> references,
            ImmutableArray<byte> publicKey,
            ImmutableArray<KeyValuePair<string, string>> pathMap,
            DeterministicKeyOptions options,
            CancellationToken cancellationToken)
        {
            writer.WriteObjectStart();
            writeToolsVersions();

            WriteByteArrayValue(writer, "publicKey", publicKey.AsSpan());
            writer.WriteKey("options");
            WriteCompilationOptions(writer, compilationOptions);

            writer.WriteKey("syntaxTrees");
            writer.WriteArrayStart();
            foreach (var syntaxTree in syntaxTrees)
            {
                cancellationToken.ThrowIfCancellationRequested();
                WriteSyntaxTree(writer, syntaxTree, pathMap, options, cancellationToken);
            }
            writer.WriteArrayEnd();

            writer.WriteKey("references");
            writer.WriteArrayStart();
            foreach (var reference in references)
            {
                cancellationToken.ThrowIfCancellationRequested();
                WriteMetadataReference(writer, reference, pathMap, options, cancellationToken);
            }
            writer.WriteArrayEnd();
            writer.WriteObjectEnd();

            void writeToolsVersions()
            {
                writer.WriteKey("toolsVersions");
                writer.WriteObjectStart();
                if ((options & DeterministicKeyOptions.IgnoreToolVersions) == 0)
                {
                    var compilerVersion = typeof(Compilation).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
                    writer.Write("compilerVersion", compilerVersion);

                    var runtimeVersion = typeof(object).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
                    writer.Write("runtimeVersion", runtimeVersion);

                    writer.Write("frameworkDescription", RuntimeInformation.FrameworkDescription);
                    writer.Write("osDescription", RuntimeInformation.OSDescription);
                }

                writer.WriteObjectEnd();
            }
        }

        private void WriteSyntaxTree(
            JsonWriter writer,
            SyntaxTreeKey syntaxTree,
            ImmutableArray<KeyValuePair<string, string>> pathMap,
            DeterministicKeyOptions options,
            CancellationToken cancellationToken)
        {
            writer.WriteObjectStart();
            WriteFilePath(writer, "fileName", syntaxTree.FilePath, pathMap, options);
            writer.WriteKey("text");
            WriteSourceText(writer, syntaxTree.GetText(cancellationToken));
            writer.WriteKey("parseOptions");
            WriteParseOptions(writer, syntaxTree.Options);
            writer.WriteObjectEnd();
        }

        private void WriteSourceText(JsonWriter writer, SourceText? sourceText)
        {
            if (sourceText is null)
            {
                writer.WriteNull();
                return;
            }

            writer.WriteObjectStart();
            WriteByteArrayValue(writer, "checksum", sourceText.GetChecksum().AsSpan());
            writer.Write("checksumAlgorithm", sourceText.ChecksumAlgorithm);
            writer.Write("encodingName", sourceText.Encoding?.EncodingName);
            writer.WriteObjectEnd();
        }

        internal void WriteMetadataReference(
            JsonWriter writer,
            MetadataReference reference,
            ImmutableArray<KeyValuePair<string, string>> pathMap,
            DeterministicKeyOptions deterministicKeyOptions,
            CancellationToken cancellationToken)
        {
            writer.WriteObjectStart();
            if (reference is PortableExecutableReference peReference)
            {
                switch (peReference.GetMetadata())
                {
                    case AssemblyMetadata assemblyMetadata:
                        {
                            var modules = assemblyMetadata.GetModules();
                            writeModuleMetadata(modules[0]);
                            writer.WriteKey("secondaryModules");
                            writer.WriteArrayStart();
                            for (var i = 1; i < modules.Length; i++)
                            {
                                writer.WriteObjectStart();
                                writeModuleMetadata(modules[i]);
                                writer.WriteObjectEnd();
                            }
                            writer.WriteArrayEnd();
                        }
                        break;
                    case ModuleMetadata m:
                        writeModuleMetadata(m);
                        break;
                    case var m:
                        throw ExceptionUtilities.UnexpectedValue(m);
                }

                writer.WriteKey("properties");
                writeMetadataReferenceProperties(writer, reference.Properties);

            }
            else if (reference is CompilationReference compilationReference)
            {
                writer.WriteKey("compilation");
                var compilation = compilationReference.Compilation;
                var builder = compilation.Options.CreateDeterministicKeyBuilder();
                builder.WriteCompilation(
                    writer,
                    compilation.Options,
                    compilation.SyntaxTrees.SelectAsArray(static x => SyntaxTreeKey.Create(x)),
                    compilation.References.AsImmutable(),
                    compilation.Assembly.Identity.PublicKey,
                    pathMap,
                    deterministicKeyOptions,
                    cancellationToken);
            }
            else
            {
                throw ExceptionUtilities.UnexpectedValue(reference);
            }

            writer.WriteObjectEnd();

            void writeModuleMetadata(ModuleMetadata moduleMetadata)
            {
                // The path of a reference, unlike the path of a file, does not contribute to the output
                // of the compilation. Only the MVID, name and version contribute here hence the file path
                // is deliberately omitted here.
                var peReader = moduleMetadata.GetMetadataReader();
                if (peReader.IsAssembly)
                {
                    var assemblyDef = peReader.GetAssemblyDefinition();
                    writer.Write("name", peReader.GetString(assemblyDef.Name));
                    WriteVersion(writer, "version", assemblyDef.Version);
                    WriteByteArrayValue(writer, "publicKey", peReader.GetBlobBytes(assemblyDef.PublicKey).AsSpan());
                }
                else
                {
                    var moduleDef = peReader.GetModuleDefinition();
                    writer.Write("name", peReader.GetString(moduleDef.Name));
                }

                writer.Write("mvid", GetGuidValue(moduleMetadata.GetModuleVersionId()));
            }

            static void writeMetadataReferenceProperties(JsonWriter writer, MetadataReferenceProperties properties)
            {
                writer.WriteObjectStart();
                writer.Write("kind", properties.Kind);
                writer.Write("embedInteropTypes", properties.EmbedInteropTypes);
                writer.WriteKey("aliases");
                writer.WriteArrayStart();
                foreach (var alias in properties.Aliases)
                {
                    writer.Write(alias);
                }
                writer.WriteArrayEnd();
                writer.WriteObjectEnd();
            }
        }

        private void WriteEmitOptions(
            JsonWriter writer,
            EmitOptions? options,
            ImmutableArray<KeyValuePair<string, string>> pathMap,
            DeterministicKeyOptions deterministicKeyOptions)
        {
            if (options is null)
            {
                writer.WriteNull();
                return;
            }

            writer.WriteObjectStart();
            writer.Write("emitMetadataOnly", options.EmitMetadataOnly);
            writer.Write("tolerateErrors", options.TolerateErrors);
            writer.Write("includePrivateMembers", options.IncludePrivateMembers);
            writer.WriteKey("instrumentationKinds");
            writer.WriteArrayStart();
            if (!options.InstrumentationKinds.IsDefault)
            {
                foreach (var kind in options.InstrumentationKinds)
                {
                    writer.Write(kind);
                }
            }
            writer.WriteArrayEnd();

            writeSubsystemVersion(writer, options.SubsystemVersion);
            writer.Write("fileAlignment", options.FileAlignment);
            writer.Write("highEntropyVirtualAddressSpace", options.HighEntropyVirtualAddressSpace);
            writer.WriteInvariant("baseAddress", options.BaseAddress);
            writer.Write("debugInformationFormat", options.DebugInformationFormat);
            writer.Write("outputNameOverride", options.OutputNameOverride);
            WriteFilePath(writer, "pdbFilePath", options.PdbFilePath, pathMap, deterministicKeyOptions);
            writer.Write("pdbChecksumAlgorithm", options.PdbChecksumAlgorithm.Name);
            writer.Write("runtimeMetadataVersion", options.RuntimeMetadataVersion);
            writer.Write("defaultSourceFileEncoding", options.DefaultSourceFileEncoding?.CodePage);
            writer.Write("fallbackSourceFileEncoding", options.FallbackSourceFileEncoding?.CodePage);
            writer.WriteObjectEnd();

            static void writeSubsystemVersion(JsonWriter writer, SubsystemVersion version)
            {
                writer.WriteKey("subsystemVersion");
                writer.WriteObjectStart();
                writer.Write("major", version.Major);
                writer.Write("minor", version.Minor);
                writer.WriteObjectEnd();
            }
        }

        private void WriteCompilationOptions(JsonWriter writer, CompilationOptions options)
        {
            writer.WriteObjectStart();
            WriteCompilationOptionsCore(writer, options);
            writer.WriteObjectEnd();
        }

        protected virtual void WriteCompilationOptionsCore(JsonWriter writer, CompilationOptions options)
        {
            // CompilationOption values
            writer.Write("outputKind", options.OutputKind);
            writer.Write("moduleName", options.ModuleName);
            writer.Write("scriptClassName", options.ScriptClassName);
            writer.Write("mainTypeName", options.MainTypeName);
            WriteByteArrayValue(writer, "cryptoPublicKey", options.CryptoPublicKey.AsSpan());
            writer.Write("cryptoKeyFile", options.CryptoKeyFile);
            writer.Write("delaySign", options.DelaySign);
            writer.Write("publicSign", options.PublicSign);
            writer.Write("checkOverflow", options.CheckOverflow);
            writer.Write("platform", options.Platform);
            writer.Write("optimizationLevel", options.OptimizationLevel);
            writer.Write("generalDiagnosticOption", options.GeneralDiagnosticOption);
            writer.Write("warningLevel", options.WarningLevel);
            writer.Write("deterministic", options.Deterministic);
            writer.Write("debugPlusMode", options.DebugPlusMode);
            writer.Write("referencesSupersedeLowerVersions", options.ReferencesSupersedeLowerVersions);
            writer.Write("reportSuppressedDiagnostics", options.ReportSuppressedDiagnostics);
            writer.Write("nullableContextOptions", options.NullableContextOptions);

            writer.WriteKey("specificDiagnosticOptions");
            writer.WriteArrayStart();
            foreach (var key in options.SpecificDiagnosticOptions.Keys.OrderBy(StringComparer.Ordinal))
            {
                writer.WriteObjectStart();
                writer.Write(key, options.SpecificDiagnosticOptions[key]);
                writer.WriteObjectEnd();
            }
            writer.WriteArrayEnd();

            if (options.Deterministic)
            {
                writer.Write("deterministic", true);
                writer.WriteNull("localtime");
            }
            else
            {
                writer.Write("deterministic", false);
                writer.WriteInvariant("localtime", options.CurrentLocalTime);

                // When using /deterministic- the compiler will *always* emit different binaries hence the 
                // key we generate here also must be different. We cannot depend on the `localtime` property
                // to provide this as the same compilation can occur on different machines at the same 
                // time. Force the issue here.
                writer.Write("nondeterministicMvid", GetGuidValue(Guid.NewGuid()));
            }

            // Values which do not impact build success / failure
            // - ConcurrentBuild
            // - MetadataImportOptions:
            // - Options.Features: deprecated
            // 

            // Not really options but they can impact compilation so we record the types. For the majority
            // of compilations this is roughly the equivalent of recording the compiler version but it 
            // could differ when customers host the compiler via the API.
            writer.WriteKey("extensions");
            writer.WriteObjectStart();

            WriteType(writer, "syntaxTreeOptionsProvider", options.SyntaxTreeOptionsProvider?.GetType());
            WriteType(writer, "metadataReferenceResolver", options.MetadataReferenceResolver?.GetType());
            WriteType(writer, "xmlReferenceResolver", options.XmlReferenceResolver?.GetType());
            WriteType(writer, "sourceReferenceResolver", options.SourceReferenceResolver?.GetType());
            WriteType(writer, "strongNameProvider", options.StrongNameProvider?.GetType());
            WriteType(writer, "assemblyIdentityComparer", options.AssemblyIdentityComparer?.GetType());
            writer.WriteObjectEnd();
        }

        protected void WriteParseOptions(JsonWriter writer, ParseOptions parseOptions)
        {
            writer.WriteObjectStart();
            WriteParseOptionsCore(writer, parseOptions);
            writer.WriteObjectEnd();
        }

        protected virtual void WriteParseOptionsCore(JsonWriter writer, ParseOptions parseOptions)
        {
            writer.Write("kind", parseOptions.Kind);
            writer.Write("specifiedKind", parseOptions.SpecifiedKind);
            writer.Write("documentationMode", parseOptions.DocumentationMode);
            writer.Write("language", parseOptions.Language);

            writer.WriteKey("features");
            var features = parseOptions.Features;
            writer.WriteObjectStart();
            foreach (var key in features.Keys.OrderBy(StringComparer.Ordinal))
            {
                writer.Write(key, features[key]);
            }
            writer.WriteObjectEnd();

            // Skipped values
            // - Errors: not sure if we need that in the key file or not
            // - PreprocessorSymbolNames: handled at the language specific level
        }
    }
}
