// 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.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Diagnostics
{
    internal static class DiagnosticHelper
    {
        /// <summary>
        /// Creates a <see cref="Diagnostic"/> instance.
        /// </summary>
        /// <param name="descriptor">A <see cref="DiagnosticDescriptor"/> describing the diagnostic.</param>
        /// <param name="location">An optional primary location of the diagnostic. If null, <see cref="Location"/> will return <see cref="Location.None"/>.</param>
        /// <param name="effectiveSeverity">Effective severity of the diagnostic.</param>
        /// <param name="additionalLocations">
        /// An optional set of additional locations related to the diagnostic.
        /// Typically, these are locations of other items referenced in the message.
        /// If null, <see cref="Diagnostic.AdditionalLocations"/> will return an empty list.
        /// </param>
        /// <param name="properties">
        /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic
        /// can convey more detailed information to the fixer. If null, <see cref="Diagnostic.Properties"/> will return
        /// <see cref="ImmutableDictionary{TKey, TValue}.Empty"/>.
        /// </param>
        /// <param name="messageArgs">Arguments to the message of the diagnostic.</param>
        /// <returns>The <see cref="Diagnostic"/> instance.</returns>
        public static Diagnostic Create(
            DiagnosticDescriptor descriptor,
            Location location,
            ReportDiagnostic effectiveSeverity,
            IEnumerable<Location>? additionalLocations,
            ImmutableDictionary<string, string?>? properties,
            params object[] messageArgs)
        {
            if (descriptor == null)
            {
                throw new ArgumentNullException(nameof(descriptor));
            }

            LocalizableString message;
            if (messageArgs == null || messageArgs.Length == 0)
            {
                message = descriptor.MessageFormat;
            }
            else
            {
                message = new LocalizableStringWithArguments(descriptor.MessageFormat, messageArgs);
            }

            return CreateWithMessage(descriptor, location, effectiveSeverity, additionalLocations, properties, message);
        }

        /// <summary>
        /// Create a diagnostic that adds properties specifying a tag for a set of locations.
        /// </summary>
        /// <param name="descriptor">A <see cref="DiagnosticDescriptor"/> describing the diagnostic.</param>
        /// <param name="location">An optional primary location of the diagnostic. If null, <see cref="Location"/> will return <see cref="Location.None"/>.</param>
        /// <param name="effectiveSeverity">Effective severity of the diagnostic.</param>
        /// <param name="additionalLocations">
        /// An optional set of additional locations related to the diagnostic.
        /// Typically, these are locations of other items referenced in the message.
        /// These locations are joined with <paramref name="additionalUnnecessaryLocations"/> to produce the value for
        /// <see cref="Diagnostic.AdditionalLocations"/>.
        /// </param>
        /// <param name="additionalUnnecessaryLocations">
        /// An optional set of additional locations indicating unnecessary code related to the diagnostic.
        /// These locations are joined with <paramref name="additionalLocations"/> to produce the value for
        /// <see cref="Diagnostic.AdditionalLocations"/>.
        /// </param>
        /// <param name="messageArgs">Arguments to the message of the diagnostic.</param>
        /// <returns>The <see cref="Diagnostic"/> instance.</returns>
        public static Diagnostic CreateWithLocationTags(
            DiagnosticDescriptor descriptor,
            Location location,
            ReportDiagnostic effectiveSeverity,
            ImmutableArray<Location> additionalLocations,
            ImmutableArray<Location> additionalUnnecessaryLocations,
            params object[] messageArgs)
        {
            if (additionalUnnecessaryLocations.IsEmpty)
            {
                return Create(descriptor, location, effectiveSeverity, additionalLocations, ImmutableDictionary<string, string?>.Empty, messageArgs);
            }

            var tagIndices = ImmutableDictionary<string, IEnumerable<int>>.Empty
                .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length));
            return CreateWithLocationTags(
                descriptor,
                location,
                effectiveSeverity,
                additionalLocations.AddRange(additionalUnnecessaryLocations),
                tagIndices,
                ImmutableDictionary<string, string?>.Empty,
                messageArgs);
        }

        /// <summary>
        /// Create a diagnostic that adds properties specifying a tag for a set of locations.
        /// </summary>
        /// <param name="descriptor">A <see cref="DiagnosticDescriptor"/> describing the diagnostic.</param>
        /// <param name="location">An optional primary location of the diagnostic. If null, <see cref="Location"/> will return <see cref="Location.None"/>.</param>
        /// <param name="effectiveSeverity">Effective severity of the diagnostic.</param>
        /// <param name="additionalLocations">
        /// An optional set of additional locations related to the diagnostic.
        /// Typically, these are locations of other items referenced in the message.
        /// These locations are joined with <paramref name="additionalUnnecessaryLocations"/> to produce the value for
        /// <see cref="Diagnostic.AdditionalLocations"/>.
        /// </param>
        /// <param name="additionalUnnecessaryLocations">
        /// An optional set of additional locations indicating unnecessary code related to the diagnostic.
        /// These locations are joined with <paramref name="additionalLocations"/> to produce the value for
        /// <see cref="Diagnostic.AdditionalLocations"/>.
        /// </param>
        /// <param name="properties">
        /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic
        /// can convey more detailed information to the fixer.
        /// </param>
        /// <param name="messageArgs">Arguments to the message of the diagnostic.</param>
        /// <returns>The <see cref="Diagnostic"/> instance.</returns>
        public static Diagnostic CreateWithLocationTags(
            DiagnosticDescriptor descriptor,
            Location location,
            ReportDiagnostic effectiveSeverity,
            ImmutableArray<Location> additionalLocations,
            ImmutableArray<Location> additionalUnnecessaryLocations,
            ImmutableDictionary<string, string?>? properties,
            params object[] messageArgs)
        {
            if (additionalUnnecessaryLocations.IsEmpty)
            {
                return Create(descriptor, location, effectiveSeverity, additionalLocations, properties, messageArgs);
            }

            var tagIndices = ImmutableDictionary<string, IEnumerable<int>>.Empty
                .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length));
            return CreateWithLocationTags(
                descriptor,
                location,
                effectiveSeverity,
                additionalLocations.AddRange(additionalUnnecessaryLocations),
                tagIndices,
                properties,
                messageArgs);
        }

        /// <summary>
        /// Create a diagnostic that adds properties specifying a tag for a set of locations.
        /// </summary>
        /// <param name="descriptor">A <see cref="DiagnosticDescriptor"/> describing the diagnostic.</param>
        /// <param name="location">An optional primary location of the diagnostic. If null, <see cref="Location"/> will return <see cref="Location.None"/>.</param>
        /// <param name="effectiveSeverity">Effective severity of the diagnostic.</param>
        /// <param name="additionalLocations">
        /// An optional set of additional locations related to the diagnostic.
        /// Typically, these are locations of other items referenced in the message.
        /// </param>
        /// <param name="tagIndices">
        /// a map of location tag to index in additional locations.
        /// "AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer" for an example of usage.
        /// </param>
        /// <param name="properties">
        /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic
        /// can convey more detailed information to the fixer.
        /// </param>
        /// <param name="messageArgs">Arguments to the message of the diagnostic.</param>
        /// <returns>The <see cref="Diagnostic"/> instance.</returns>
        private static Diagnostic CreateWithLocationTags(
            DiagnosticDescriptor descriptor,
            Location location,
            ReportDiagnostic effectiveSeverity,
            IEnumerable<Location> additionalLocations,
            IDictionary<string, IEnumerable<int>> tagIndices,
            ImmutableDictionary<string, string?>? properties,
            params object[] messageArgs)
        {
            Contract.ThrowIfTrue(additionalLocations.IsEmpty());
            Contract.ThrowIfTrue(tagIndices.IsEmpty());

            properties ??= ImmutableDictionary<string, string?>.Empty;
            properties = properties.AddRange(tagIndices.Select(kvp => new KeyValuePair<string, string?>(kvp.Key, EncodeIndices(kvp.Value, additionalLocations.Count()))));

            return Create(descriptor, location, effectiveSeverity, additionalLocations, properties, messageArgs);

            static string EncodeIndices(IEnumerable<int> indices, int additionalLocationsLength)
            {
                // Ensure that the provided tag index is a valid index into additional locations.
                Contract.ThrowIfFalse(indices.All(idx => idx >= 0 && idx < additionalLocationsLength));

                using var stream = new MemoryStream();
                var serializer = new DataContractJsonSerializer(typeof(IEnumerable<int>));
                serializer.WriteObject(stream, indices);

                var jsonBytes = stream.ToArray();
                stream.Close();
                return Encoding.UTF8.GetString(jsonBytes, 0, jsonBytes.Length);
            }
        }

        /// <summary>
        /// Creates a <see cref="Diagnostic"/> instance.
        /// </summary>
        /// <param name="descriptor">A <see cref="DiagnosticDescriptor"/> describing the diagnostic.</param>
        /// <param name="location">An optional primary location of the diagnostic. If null, <see cref="Location"/> will return <see cref="Location.None"/>.</param>
        /// <param name="effectiveSeverity">Effective severity of the diagnostic.</param>
        /// <param name="additionalLocations">
        /// An optional set of additional locations related to the diagnostic.
        /// Typically, these are locations of other items referenced in the message.
        /// If null, <see cref="Diagnostic.AdditionalLocations"/> will return an empty list.
        /// </param>
        /// <param name="properties">
        /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic
        /// can convey more detailed information to the fixer. If null, <see cref="Diagnostic.Properties"/> will return
        /// <see cref="ImmutableDictionary{TKey, TValue}.Empty"/>.
        /// </param>
        /// <param name="message">Localizable message for the diagnostic.</param>
        /// <returns>The <see cref="Diagnostic"/> instance.</returns>
        public static Diagnostic CreateWithMessage(
            DiagnosticDescriptor descriptor,
            Location location,
            ReportDiagnostic effectiveSeverity,
            IEnumerable<Location>? additionalLocations,
            ImmutableDictionary<string, string?>? properties,
            LocalizableString message)
        {
            if (descriptor == null)
            {
                throw new ArgumentNullException(nameof(descriptor));
            }

            return Diagnostic.Create(
                descriptor.Id,
                descriptor.Category,
                message,
                effectiveSeverity.ToDiagnosticSeverity() ?? descriptor.DefaultSeverity,
                descriptor.DefaultSeverity,
                descriptor.IsEnabledByDefault,
                warningLevel: effectiveSeverity.WithDefaultSeverity(descriptor.DefaultSeverity) == ReportDiagnostic.Error ? 0 : 1,
                effectiveSeverity == ReportDiagnostic.Suppress,
                descriptor.Title,
                descriptor.Description,
                descriptor.HelpLinkUri,
                location,
                additionalLocations,
                descriptor.CustomTags,
                properties);
        }

        public static string? GetHelpLinkForDiagnosticId(string id)
        {
            // TODO: Add documentation for Regex and Json analyzer
            // Tracked with https://github.com/dotnet/roslyn/issues/48530
            if (id == "RE0001")
                return null;

            if (id.StartsWith("JSON", StringComparison.Ordinal))
                return null;

            // These diagnostics are hidden and not configurable, so help link can never be shown and is not applicable.
            if (id == RemoveUnnecessaryImports.RemoveUnnecessaryImportsConstants.DiagnosticFixableId ||
                id == "IDE0005_gen")
            {
                return null;
            }

            Debug.Assert(id.StartsWith("IDE", StringComparison.Ordinal));
            return $"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/{id.ToLowerInvariant()}";
        }

        public sealed class LocalizableStringWithArguments : LocalizableString, IObjectWritable
        {
            private readonly LocalizableString _messageFormat;
            private readonly string[] _formatArguments;

            static LocalizableStringWithArguments()
                => ObjectBinder.RegisterTypeReader(typeof(LocalizableStringWithArguments), reader => new LocalizableStringWithArguments(reader));

            public LocalizableStringWithArguments(LocalizableString messageFormat, params object[] formatArguments)
            {
                if (messageFormat == null)
                {
                    throw new ArgumentNullException(nameof(messageFormat));
                }

                if (formatArguments == null)
                {
                    throw new ArgumentNullException(nameof(formatArguments));
                }

                _messageFormat = messageFormat;
                _formatArguments = new string[formatArguments.Length];
                for (var i = 0; i < formatArguments.Length; i++)
                {
                    _formatArguments[i] = $"{formatArguments[i]}";
                }
            }

            private LocalizableStringWithArguments(ObjectReader reader)
            {
                _messageFormat = (LocalizableString)reader.ReadValue();

                var length = reader.ReadInt32();
                if (length == 0)
                {
                    _formatArguments = Array.Empty<string>();
                }
                else
                {
                    using var _ = ArrayBuilder<string>.GetInstance(length, out var argumentsBuilder);
                    for (var i = 0; i < length; i++)
                    {
                        argumentsBuilder.Add(reader.ReadString());
                    }

                    _formatArguments = argumentsBuilder.ToArray();
                }
            }

            bool IObjectWritable.ShouldReuseInSerialization => false;

            void IObjectWritable.WriteTo(ObjectWriter writer)
            {
                writer.WriteValue(_messageFormat);
                var length = _formatArguments.Length;
                writer.WriteInt32(length);
                for (var i = 0; i < length; i++)
                {
                    writer.WriteString(_formatArguments[i]);
                }
            }

            protected override string GetText(IFormatProvider? formatProvider)
            {
                var messageFormat = _messageFormat.ToString(formatProvider);
                return messageFormat != null
                    ? (_formatArguments.Length > 0 ? string.Format(formatProvider, messageFormat, _formatArguments) : messageFormat)
                    : string.Empty;
            }

            protected override bool AreEqual(object? other)
            {
                return other is LocalizableStringWithArguments otherResourceString &&
                    _messageFormat.Equals(otherResourceString._messageFormat) &&
                    _formatArguments.SequenceEqual(otherResourceString._formatArguments, (a, b) => a == b);
            }

            protected override int GetHash()
            {
                return Hash.Combine(
                    _messageFormat.GetHashCode(),
                    Hash.CombineValues(_formatArguments));
            }
        }
    }
}
