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

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;

namespace Microsoft.Extensions.DependencyModel
{
    public class DependencyContextWriter
    {
        public void Write(DependencyContext context, Stream stream)
        {
            ThrowHelper.ThrowIfNull(context);
            ThrowHelper.ThrowIfNull(stream);

            // Custom encoder is required to fix https://github.com/dotnet/runtime/issues/3678
            // Since the JSON is only written to a file that is read by the SDK (and not transmitted over the wire),
            // it is safe to skip escaping certain characters in this scenario
            // (that would otherwise be escaped, by default, as part of defense-in-depth, such as +).
            var options = new JsonWriterOptions { Indented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
            using (var jsonWriter = new Utf8JsonWriter(stream, options))
            {
                jsonWriter.WriteStartObject();
                WriteRuntimeTargetInfo(context, jsonWriter);
                WriteCompilationOptions(context.CompilationOptions, jsonWriter);
                WriteTargets(context, jsonWriter);
                WriteLibraries(context, jsonWriter);
                if (context.RuntimeGraph.Any())
                {
                    WriteRuntimeGraph(context, jsonWriter);
                }
                jsonWriter.WriteEndObject();
            }
        }

        private static void WriteRuntimeTargetInfo(DependencyContext context, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(DependencyContextStrings.RuntimeTargetPropertyName);
            if (context.Target.IsPortable)
            {
                jsonWriter.WriteString(DependencyContextStrings.RuntimeTargetNamePropertyName,
                    context.Target.Framework);
            }
            else
            {
                jsonWriter.WriteString(DependencyContextStrings.RuntimeTargetNamePropertyName,
                    context.Target.Framework + DependencyContextStrings.VersionSeparator + context.Target.Runtime);
            }
            jsonWriter.WriteString(DependencyContextStrings.RuntimeTargetSignaturePropertyName,
                context.Target.RuntimeSignature);
            jsonWriter.WriteEndObject();
        }

        private static void WriteRuntimeGraph(DependencyContext context, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(DependencyContextStrings.RuntimesPropertyName);
            foreach (RuntimeFallbacks runtimeFallback in context.RuntimeGraph)
            {
                jsonWriter.WriteStartArray(runtimeFallback.Runtime);
                foreach (string? fallback in runtimeFallback.Fallbacks)
                {
                    jsonWriter.WriteStringValue(fallback);
                }
                jsonWriter.WriteEndArray();
            }
            jsonWriter.WriteEndObject();
        }

        private static void WriteCompilationOptions(CompilationOptions compilationOptions, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(DependencyContextStrings.CompilationOptionsPropertyName);
            if (compilationOptions.Defines?.Any() == true)
            {
                jsonWriter.WriteStartArray(DependencyContextStrings.DefinesPropertyName);
                foreach (string? define in compilationOptions.Defines)
                {
                    jsonWriter.WriteStringValue(define);
                }
                jsonWriter.WriteEndArray();
            }
            AddStringPropertyIfNotNull(DependencyContextStrings.LanguageVersionPropertyName, compilationOptions.LanguageVersion, jsonWriter);
            AddStringPropertyIfNotNull(DependencyContextStrings.PlatformPropertyName, compilationOptions.Platform, jsonWriter);
            AddBooleanPropertyIfNotNull(DependencyContextStrings.AllowUnsafePropertyName, compilationOptions.AllowUnsafe, jsonWriter);
            AddBooleanPropertyIfNotNull(DependencyContextStrings.WarningsAsErrorsPropertyName, compilationOptions.WarningsAsErrors, jsonWriter);
            AddBooleanPropertyIfNotNull(DependencyContextStrings.OptimizePropertyName, compilationOptions.Optimize, jsonWriter);
            AddStringPropertyIfNotNull(DependencyContextStrings.KeyFilePropertyName, compilationOptions.KeyFile, jsonWriter);
            AddBooleanPropertyIfNotNull(DependencyContextStrings.DelaySignPropertyName, compilationOptions.DelaySign, jsonWriter);
            AddBooleanPropertyIfNotNull(DependencyContextStrings.PublicSignPropertyName, compilationOptions.PublicSign, jsonWriter);
            AddBooleanPropertyIfNotNull(DependencyContextStrings.EmitEntryPointPropertyName, compilationOptions.EmitEntryPoint, jsonWriter);
            AddBooleanPropertyIfNotNull(DependencyContextStrings.GenerateXmlDocumentationPropertyName, compilationOptions.GenerateXmlDocumentation, jsonWriter);
            AddStringPropertyIfNotNull(DependencyContextStrings.DebugTypePropertyName, compilationOptions.DebugType, jsonWriter);
            jsonWriter.WriteEndObject();
        }

        private static void AddStringPropertyIfNotNull(string name, string? value, Utf8JsonWriter jsonWriter)
        {
            if (value != null)
            {
                jsonWriter.WriteString(name, value);
            }
        }

        private static void AddBooleanPropertyIfNotNull(string name, bool? value, Utf8JsonWriter jsonWriter)
        {
            if (value.HasValue)
            {
                jsonWriter.WriteBoolean(name, value.Value);
            }
        }

        private static void WriteTargets(DependencyContext context, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(DependencyContextStrings.TargetsPropertyName);
            if (context.Target.IsPortable)
            {
                WritePortableTarget(context.Target.Framework, context.RuntimeLibraries, context.CompileLibraries, jsonWriter);
            }
            else
            {
                WriteTarget(context.Target.Framework, context.CompileLibraries, jsonWriter);
                WriteTarget(context.Target.Framework + DependencyContextStrings.VersionSeparator + context.Target.Runtime,
                    context.RuntimeLibraries, jsonWriter);
            }
            jsonWriter.WriteEndObject();
        }

        private static void WriteTarget(string key, IReadOnlyList<Library> libraries, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(key);
            int count = libraries.Count;
            for (int i = 0; i < count; i++)
            {
                Library library = libraries[i];
                WriteTargetLibrary(library.Name + DependencyContextStrings.VersionSeparator + library.Version, library, jsonWriter);
            }
            jsonWriter.WriteEndObject();
        }

        private static void WritePortableTarget(string key, IReadOnlyList<RuntimeLibrary> runtimeLibraries, IReadOnlyList<CompilationLibrary> compilationLibraries, Utf8JsonWriter jsonWriter)
        {
            Dictionary<string, RuntimeLibrary> runtimeLookup = runtimeLibraries.LibraryCollectionToDictionary();
            Dictionary<string, CompilationLibrary> compileLookup = compilationLibraries.LibraryCollectionToDictionary();

            jsonWriter.WriteStartObject(key);

            foreach (string packageName in runtimeLookup.Keys.Concat(compileLookup.Keys).Distinct())
            {
                runtimeLookup.TryGetValue(packageName, out RuntimeLibrary? runtimeLibrary);

                compileLookup.TryGetValue(packageName, out CompilationLibrary? compilationLibrary);

                if (compilationLibrary != null && runtimeLibrary != null)
                {
                    Debug.Assert(compilationLibrary.Serviceable == runtimeLibrary.Serviceable);
                    Debug.Assert(compilationLibrary.Version == runtimeLibrary.Version);
                    Debug.Assert(compilationLibrary.Hash == runtimeLibrary.Hash);
                    Debug.Assert(compilationLibrary.Type == runtimeLibrary.Type);
                    Debug.Assert(compilationLibrary.Path == runtimeLibrary.Path);
                    Debug.Assert(compilationLibrary.HashPath == runtimeLibrary.HashPath);
                    Debug.Assert(compilationLibrary.RuntimeStoreManifestName == null);
                }

                Library library = (Library?)compilationLibrary ?? (Library)runtimeLibrary!;

                WritePortableTargetLibrary(library.Name + DependencyContextStrings.VersionSeparator + library.Version,
                    runtimeLibrary, compilationLibrary, jsonWriter);
            }
            jsonWriter.WriteEndObject();
        }

        private static void AddCompilationAssemblies(IEnumerable<string> compilationAssemblies, Utf8JsonWriter jsonWriter)
        {
            if (!compilationAssemblies.Any())
            {
                return;
            }

            WriteAssetList(DependencyContextStrings.CompileTimeAssembliesKey, compilationAssemblies, jsonWriter);
        }

        private static void AddAssets(string key, RuntimeAssetGroup? group, Utf8JsonWriter jsonWriter)
        {
            if (group == null || !group.RuntimeFiles.Any())
            {
                return;
            }

            WriteAssetList(key, group.RuntimeFiles, jsonWriter);
        }

        private static void AddDependencies(IReadOnlyCollection<Dependency> dependencies, Utf8JsonWriter jsonWriter)
        {
            if (dependencies.Count == 0)
            {
                return;
            }

            jsonWriter.WriteStartObject(DependencyContextStrings.DependenciesPropertyName);
            foreach (Dependency dependency in dependencies)
            {
                jsonWriter.WriteString(dependency.Name, dependency.Version);
            }
            jsonWriter.WriteEndObject();
        }

        private static void AddResourceAssemblies(IReadOnlyList<ResourceAssembly> resourceAssemblies, Utf8JsonWriter jsonWriter)
        {
            int count = resourceAssemblies.Count;
            if (count == 0)
            {
                return;
            }

            jsonWriter.WriteStartObject(DependencyContextStrings.ResourceAssembliesPropertyName);
            for (int i = 0; i < count; i++)
            {
                ResourceAssembly resourceAssembly = resourceAssemblies[i];
                jsonWriter.WriteStartObject(NormalizePath(resourceAssembly.Path));
                jsonWriter.WriteString(DependencyContextStrings.LocalePropertyName, resourceAssembly.Locale);
                jsonWriter.WriteEndObject();
            }
            jsonWriter.WriteEndObject();
        }

        private static void WriteTargetLibrary(string key, Library library, Utf8JsonWriter jsonWriter)
        {
            if (library is RuntimeLibrary runtimeLibrary)
            {
                jsonWriter.WriteStartObject(key);

                AddDependencies(runtimeLibrary.Dependencies, jsonWriter);

                // Add runtime-agnostic assets
                AddAssets(DependencyContextStrings.RuntimeAssembliesKey, runtimeLibrary.RuntimeAssemblyGroups.GetDefaultGroup(), jsonWriter);
                AddAssets(DependencyContextStrings.NativeLibrariesKey, runtimeLibrary.NativeLibraryGroups.GetDefaultGroup(), jsonWriter);
                AddResourceAssemblies(runtimeLibrary.ResourceAssemblies, jsonWriter);

                jsonWriter.WriteEndObject();
            }
            else if (library is CompilationLibrary compilationLibrary)
            {
                jsonWriter.WriteStartObject(key);
                AddDependencies(compilationLibrary.Dependencies, jsonWriter);
                AddCompilationAssemblies(compilationLibrary.Assemblies, jsonWriter);
                jsonWriter.WriteEndObject();
            }
            else
            {
                throw new NotSupportedException();
            }
        }

        private static void WritePortableTargetLibrary(string key, RuntimeLibrary? runtimeLibrary, CompilationLibrary? compilationLibrary, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(key);

            var dependencies = new HashSet<Dependency>();
            if (runtimeLibrary != null)
            {
                dependencies.UnionWith(runtimeLibrary.Dependencies);
            }

            if (compilationLibrary != null)
            {
                dependencies.UnionWith(compilationLibrary.Dependencies);
            }
            AddDependencies(dependencies, jsonWriter);


            if (runtimeLibrary != null)
            {
                // Add runtime-agnostic assets
                AddAssets(DependencyContextStrings.RuntimeAssembliesKey, runtimeLibrary.RuntimeAssemblyGroups.GetDefaultGroup(), jsonWriter);
                AddAssets(DependencyContextStrings.NativeLibrariesKey, runtimeLibrary.NativeLibraryGroups.GetDefaultGroup(), jsonWriter);
                AddResourceAssemblies(runtimeLibrary.ResourceAssemblies, jsonWriter);

                // Add runtime-specific assets
                bool wroteObjectStart = false;
                wroteObjectStart = AddRuntimeSpecificAssetGroups(DependencyContextStrings.RuntimeAssetType, runtimeLibrary.RuntimeAssemblyGroups, wroteObjectStart, jsonWriter);
                wroteObjectStart = AddRuntimeSpecificAssetGroups(DependencyContextStrings.NativeAssetType, runtimeLibrary.NativeLibraryGroups, wroteObjectStart, jsonWriter);

                if (wroteObjectStart)
                {
                    jsonWriter.WriteEndObject();
                }
            }

            if (compilationLibrary != null)
            {
                AddCompilationAssemblies(compilationLibrary.Assemblies, jsonWriter);
            }

            if (compilationLibrary != null && runtimeLibrary == null)
            {
                jsonWriter.WriteBoolean(DependencyContextStrings.CompilationOnlyPropertyName, true);
            }

            jsonWriter.WriteEndObject();
        }

        private static bool AddRuntimeSpecificAssetGroups(string assetType, IEnumerable<RuntimeAssetGroup> assetGroups, bool wroteObjectStart, Utf8JsonWriter jsonWriter)
        {
            using IEnumerator<RuntimeAssetGroup> groups = assetGroups.Where(g => !string.IsNullOrEmpty(g.Runtime)).GetEnumerator();

            if (groups.MoveNext())
            {
                if (!wroteObjectStart)
                {
                    jsonWriter.WriteStartObject(DependencyContextStrings.RuntimeTargetsPropertyName);
                    wroteObjectStart = true;
                }

                do
                {
                    RuntimeAssetGroup group = groups.Current;

                    if (group.RuntimeFiles.Count != 0)
                    {
                        AddRuntimeSpecificAssets(group.RuntimeFiles, group.Runtime, assetType, jsonWriter);
                    }
                    else
                    {
                        // Add a placeholder item
                        // We need to generate a pseudo-path because there could be multiple different asset groups with placeholders
                        // Only the last path segment matters, the rest is basically just a GUID.
                        string pseudoPathFolder = assetType == DependencyContextStrings.RuntimeAssetType ?
                            "lib" :
                            "native";

                        jsonWriter.WriteStartObject($"runtime/{group.Runtime}/{pseudoPathFolder}/_._");
                        jsonWriter.WriteString(DependencyContextStrings.RidPropertyName, group.Runtime);
                        jsonWriter.WriteString(DependencyContextStrings.AssetTypePropertyName, assetType);
                        jsonWriter.WriteEndObject();
                    }
                }
                while (groups.MoveNext());
            }

            return wroteObjectStart;
        }

        private static void AddRuntimeSpecificAssets(IEnumerable<RuntimeFile> assets, string? runtime, string? assetType, Utf8JsonWriter jsonWriter)
        {
            foreach (RuntimeFile asset in assets)
            {
                jsonWriter.WriteStartObject(NormalizePath(asset.Path));

                jsonWriter.WriteString(DependencyContextStrings.RidPropertyName, runtime);
                jsonWriter.WriteString(DependencyContextStrings.AssetTypePropertyName, assetType);

                if (asset.AssemblyVersion != null)
                {
                    jsonWriter.WriteString(DependencyContextStrings.AssemblyVersionPropertyName, asset.AssemblyVersion);
                }

                if (asset.FileVersion != null)
                {
                    jsonWriter.WriteString(DependencyContextStrings.FileVersionPropertyName, asset.FileVersion);
                }

                jsonWriter.WriteEndObject();
            }
        }

        private static void WriteAssetList(string key, IEnumerable<string> assetPaths, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(key);
            foreach (string assembly in assetPaths)
            {
                jsonWriter.WriteStartObject(NormalizePath(assembly));
                jsonWriter.WriteEndObject();
            }
            jsonWriter.WriteEndObject();
        }

        private static void WriteAssetList(string key, IEnumerable<RuntimeFile> runtimeFiles, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(key);

            foreach (RuntimeFile runtimeFile in runtimeFiles)
            {
                jsonWriter.WriteStartObject(NormalizePath(runtimeFile.Path));

                if (runtimeFile.AssemblyVersion != null)
                {
                    jsonWriter.WriteString(DependencyContextStrings.AssemblyVersionPropertyName, runtimeFile.AssemblyVersion);
                }

                if (runtimeFile.FileVersion != null)
                {
                    jsonWriter.WriteString(DependencyContextStrings.FileVersionPropertyName, runtimeFile.FileVersion);
                }

                jsonWriter.WriteEndObject();
            }

            jsonWriter.WriteEndObject();
        }

        private static void WriteLibraries(DependencyContext context, Utf8JsonWriter jsonWriter)
        {
            IEnumerable<IGrouping<string, Library>> allLibraries =
                context.RuntimeLibraries.Cast<Library>().Concat(context.CompileLibraries)
                    .GroupBy(library => library.Name + DependencyContextStrings.VersionSeparator + library.Version);

            jsonWriter.WriteStartObject(DependencyContextStrings.LibrariesPropertyName);
            foreach (IGrouping<string, Library> libraryGroup in allLibraries)
            {
                WriteLibrary(libraryGroup.Key, libraryGroup.First(), jsonWriter);
            }
            jsonWriter.WriteEndObject();
        }

        private static void WriteLibrary(string key, Library library, Utf8JsonWriter jsonWriter)
        {
            jsonWriter.WriteStartObject(key);
            jsonWriter.WriteString(DependencyContextStrings.TypePropertyName, library.Type);
            jsonWriter.WriteBoolean(DependencyContextStrings.ServiceablePropertyName, library.Serviceable);
            jsonWriter.WriteString(DependencyContextStrings.Sha512PropertyName, library.Hash);

            if (library.Path != null)
            {
                jsonWriter.WriteString(DependencyContextStrings.PathPropertyName, library.Path);
            }

            if (library.HashPath != null)
            {
                jsonWriter.WriteString(DependencyContextStrings.HashPathPropertyName, library.HashPath);
            }

            if (library.RuntimeStoreManifestName != null)
            {
                jsonWriter.WriteString(DependencyContextStrings.RuntimeStoreManifestPropertyName, library.RuntimeStoreManifestName);
            }

            jsonWriter.WriteEndObject();
        }

        private static string NormalizePath(string path)
        {
            return path.Replace('\\', '/');
        }
    }
}
