// 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.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis.Diagnostics;
using Roslyn.Utilities;

#pragma warning disable RS0013 // We need to invoke Diagnostic.Descriptor here to log all the metadata properties of the diagnostic.

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Used for logging compiler diagnostics to a stream in the unstandardized SARIF
    /// (Static Analysis Results Interchange Format) v1.0.0 format.
    /// https://github.com/sarif-standard/sarif-spec
    /// https://rawgit.com/sarif-standard/sarif-spec/main/Static%20Analysis%20Results%20Interchange%20Format%20(SARIF).html
    /// </summary>
    /// <remarks>
    /// To log diagnostics in the standardized SARIF v2.1.0 format, use the SarifV2ErrorLogger.
    /// </remarks>
    internal sealed class SarifV1ErrorLogger : SarifErrorLogger, IDisposable
    {
        private readonly DiagnosticDescriptorSet _descriptors;
        public SarifV1ErrorLogger(Stream stream, string toolName, string toolFileVersion, Version toolAssemblyVersion, CultureInfo culture)
            : base(stream, culture)
        {
            _descriptors = new DiagnosticDescriptorSet();

            _writer.WriteObjectStart(); // root
            _writer.Write("$schema", "http://json.schemastore.org/sarif-1.0.0");
            _writer.Write("version", "1.0.0");
            _writer.WriteArrayStart("runs");
            _writer.WriteObjectStart(); // run

            _writer.WriteObjectStart("tool");
            _writer.Write("name", toolName);
            _writer.Write("version", toolAssemblyVersion.ToString());
            _writer.Write("fileVersion", toolFileVersion);
            _writer.Write("semanticVersion", toolAssemblyVersion.ToString(fieldCount: 3));

            // Emit the 'language' property only if it is a non-empty string to match the SARIF spec.
            if (culture.Name.Length > 0)
                _writer.Write("language", culture.Name);

            _writer.WriteObjectEnd(); // tool

            _writer.WriteArrayStart("results");
        }

        protected override string PrimaryLocationPropertyName => "resultFile";

        public override void LogDiagnostic(Diagnostic diagnostic, SuppressionInfo? suppressionInfo)
        {
            _writer.WriteObjectStart(); // result
            _writer.Write("ruleId", diagnostic.Id);

            string ruleKey = _descriptors.Add(diagnostic.Descriptor);
            if (ruleKey != diagnostic.Id)
            {
                _writer.Write("ruleKey", ruleKey);
            }

            _writer.Write("level", GetLevel(diagnostic.Severity));

            string? message = diagnostic.GetMessage(_culture);
            if (!RoslynString.IsNullOrEmpty(message))
            {
                _writer.Write("message", message);
            }

            if (diagnostic.IsSuppressed)
            {
                _writer.WriteArrayStart("suppressionStates");
                _writer.Write("suppressedInSource");
                _writer.WriteArrayEnd();
            }

            WriteLocations(diagnostic.Location, diagnostic.AdditionalLocations);
            WriteResultProperties(diagnostic);

            _writer.WriteObjectEnd(); // result
        }

        private void WriteLocations(Location location, IReadOnlyList<Location> additionalLocations)
        {
            if (HasPath(location))
            {
                _writer.WriteArrayStart("locations");
                _writer.WriteObjectStart(); // location
                _writer.WriteKey(PrimaryLocationPropertyName);

                WritePhysicalLocation(location);

                _writer.WriteObjectEnd(); // location
                _writer.WriteArrayEnd(); // locations
            }

            // See https://github.com/dotnet/roslyn/issues/11228 for discussion around
            // whether this is the correct treatment of Diagnostic.AdditionalLocations
            // as SARIF relatedLocations.
            if (additionalLocations != null &&
                additionalLocations.Count > 0 &&
                additionalLocations.Any(l => HasPath(l)))
            {
                _writer.WriteArrayStart("relatedLocations");

                foreach (var additionalLocation in additionalLocations)
                {
                    if (HasPath(additionalLocation))
                    {
                        _writer.WriteObjectStart(); // annotatedCodeLocation
                        _writer.WriteKey("physicalLocation");

                        WritePhysicalLocation(additionalLocation);

                        _writer.WriteObjectEnd(); // annotatedCodeLocation
                    }
                }

                _writer.WriteArrayEnd(); // relatedLocations
            }
        }

        public override void AddAnalyzerDescriptorsAndExecutionTime(ImmutableArray<(DiagnosticDescriptor Descriptor, DiagnosticDescriptorErrorLoggerInfo Info)> descriptors, double totalAnalyzerExecutionTime)
        {
            // We log all analyzer descriptors only in SARIF v2+ format.
        }

        protected override void WritePhysicalLocation(Location location)
        {
            Debug.Assert(HasPath(location));

            FileLinePositionSpan span = location.GetMappedLineSpan();

            _writer.WriteObjectStart();
            _writer.Write("uri", GetUri(span.Path));

            WriteRegion(span);

            _writer.WriteObjectEnd();
        }

        private void WriteRules()
        {
            if (_descriptors.Count > 0)
            {
                _writer.WriteObjectStart("rules");

                foreach (var pair in _descriptors.ToSortedList())
                {
                    DiagnosticDescriptor descriptor = pair.Value;

                    _writer.WriteObjectStart(pair.Key); // rule
                    _writer.Write("id", descriptor.Id);

                    string? shortDescription = descriptor.Title.ToString(_culture);
                    if (!RoslynString.IsNullOrEmpty(shortDescription))
                    {
                        _writer.Write("shortDescription", shortDescription);
                    }

                    string? fullDescription = descriptor.Description.ToString(_culture);
                    if (!RoslynString.IsNullOrEmpty(fullDescription))
                    {
                        _writer.Write("fullDescription", fullDescription);
                    }

                    _writer.Write("defaultLevel", GetLevel(descriptor.DefaultSeverity));

                    if (!string.IsNullOrEmpty(descriptor.HelpLinkUri))
                    {
                        _writer.Write("helpUri", descriptor.HelpLinkUri);
                    }

                    _writer.WriteObjectStart("properties");

                    if (!string.IsNullOrEmpty(descriptor.Category))
                    {
                        _writer.Write("category", descriptor.Category);
                    }

                    _writer.Write("isEnabledByDefault", descriptor.IsEnabledByDefault);

                    if (descriptor.ImmutableCustomTags.Any())
                    {
                        _writer.WriteArrayStart("tags");

                        foreach (string tag in descriptor.ImmutableCustomTags)
                        {
                            _writer.Write(tag);
                        }

                        _writer.WriteArrayEnd(); // tags
                    }

                    _writer.WriteObjectEnd(); // properties
                    _writer.WriteObjectEnd(); // rule
                }

                _writer.WriteObjectEnd(); // rules
            }
        }

        public override void Dispose()
        {
            _writer.WriteArrayEnd();  // results

            WriteRules();

            _writer.WriteObjectEnd(); // run
            _writer.WriteArrayEnd();  // runs
            _writer.WriteObjectEnd(); // root

            base.Dispose();
        }

        /// <summary>
        /// Represents a distinct set of <see cref="DiagnosticDescriptor"/>s and provides unique string keys 
        /// to distinguish them.
        ///
        /// The first <see cref="DiagnosticDescriptor"/> added with a given <see cref="DiagnosticDescriptor.Id"/>
        /// value is given that value as its unique key. Subsequent adds with the same ID will have .NNN
        /// appended to their with an auto-incremented numeric value.
        /// </summary>
        private sealed class DiagnosticDescriptorSet
        {
            // DiagnosticDescriptor.Id -> auto-incremented counter
            private readonly Dictionary<string, int> _counters = new Dictionary<string, int>();

            // DiagnosticDescriptor -> unique key
            private readonly Dictionary<DiagnosticDescriptor, string> _keys = new Dictionary<DiagnosticDescriptor, string>(SarifDiagnosticComparer.Instance);

            /// <summary>
            /// The total number of descriptors in the set.
            /// </summary>
            public int Count => _keys.Count;

            /// <summary>
            /// Adds a descriptor to the set if not already present.
            /// </summary>
            /// <returns>
            /// The unique key assigned to the given descriptor.
            /// </returns>
            public string Add(DiagnosticDescriptor descriptor)
            {
                // Case 1: Descriptor has already been seen -> retrieve key from cache.
                if (_keys.TryGetValue(descriptor, out string? key))
                {
                    return key;
                }

                // Case 2: First time we see a descriptor with a given ID -> use its ID as the key.
                if (!_counters.TryGetValue(descriptor.Id, out int counter))
                {
                    _counters.Add(descriptor.Id, 0);
                    _keys.Add(descriptor, descriptor.Id);
                    return descriptor.Id;
                }

                // Case 3: We've already seen a different descriptor with the same ID -> generate a key.
                //
                // This will only need to loop in the corner case where there is an actual descriptor 
                // with non-generated ID=X.NNN and more than one descriptor with ID=X.
                do
                {
                    _counters[descriptor.Id] = ++counter;
                    key = descriptor.Id + "-" + counter.ToString("000", CultureInfo.InvariantCulture);
                } while (_counters.ContainsKey(key));

                _keys.Add(descriptor, key);
                return key;
            }

            /// <summary>
            /// Converts the set to a list of (key, descriptor) pairs sorted by key.
            /// </summary>
            public List<KeyValuePair<string, DiagnosticDescriptor>> ToSortedList()
            {
                Debug.Assert(Count > 0);

                var list = new List<KeyValuePair<string, DiagnosticDescriptor>>(Count);

                foreach (var pair in _keys)
                {
                    Debug.Assert(list.Capacity > list.Count);
                    list.Add(new KeyValuePair<string, DiagnosticDescriptor>(pair.Value, pair.Key));
                }

                Debug.Assert(list.Capacity == list.Count);
                list.Sort((x, y) => string.CompareOrdinal(x.Key, y.Key));
                return list;
            }
        }
    }
}

#pragma warning restore RS0013
