// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the MIT license.  See License.txt in the project root for license information.

using System.Collections;
using System.Collections.Immutable;
using System.Reflection;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;

namespace Microsoft.CodeAnalysis.Tools.Analyzers
{
    public static class Extensions
    {
        private static Assembly MicrosoftCodeAnalysisFeaturesAssembly { get; }
        private static Type IDEDiagnosticIdToOptionMappingHelperType { get; }
        private static MethodInfo TryGetMappedOptionsMethod { get; }

        static Extensions()
        {
            MicrosoftCodeAnalysisFeaturesAssembly = Assembly.Load(new AssemblyName("Microsoft.CodeAnalysis.Features"));
            IDEDiagnosticIdToOptionMappingHelperType = MicrosoftCodeAnalysisFeaturesAssembly.GetType("Microsoft.CodeAnalysis.Diagnostics.IDEDiagnosticIdToOptionMappingHelper")!;
            TryGetMappedOptionsMethod = IDEDiagnosticIdToOptionMappingHelperType.GetMethod("TryGetMappedOptions", BindingFlags.Static | BindingFlags.Public)!;
        }

        public static bool Any(this SolutionChanges solutionChanges)
                => solutionChanges.GetProjectChanges()
                    .Any(x => x.GetChangedDocuments().Any() || x.GetChangedAdditionalDocuments().Any());

        /// <summary>
        /// Get the highest possible severity for any formattable document in the project.
        /// </summary>
        public static async Task<DiagnosticSeverity> GetSeverityAsync(
            this DiagnosticAnalyzer analyzer,
            Project project,
            ImmutableHashSet<string> formattablePaths,
            CancellationToken cancellationToken)
        {
            var severity = DiagnosticSeverity.Hidden;
            var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
            if (compilation is null)
            {
                return severity;
            }

            foreach (var document in project.Documents)
            {
                // Is the document formattable?
                if (document.FilePath is null || !formattablePaths.Contains(document.FilePath))
                {
                    continue;
                }

                var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);

                var documentSeverity = analyzer.GetSeverity(document, project.AnalyzerOptions, options, compilation);
                if (documentSeverity > severity)
                {
                    severity = documentSeverity;
                }
            }

            return severity;
        }

        public static DiagnosticSeverity ToSeverity(this ReportDiagnostic reportDiagnostic)
        {
            return reportDiagnostic switch
            {
                ReportDiagnostic.Error => DiagnosticSeverity.Error,
                ReportDiagnostic.Warn => DiagnosticSeverity.Warning,
                ReportDiagnostic.Info => DiagnosticSeverity.Info,
                _ => DiagnosticSeverity.Hidden
            };
        }

        private static DiagnosticSeverity GetSeverity(
            this DiagnosticAnalyzer analyzer,
            Document document,
            AnalyzerOptions? analyzerOptions,
            OptionSet options,
            Compilation compilation)
        {
            var severity = DiagnosticSeverity.Hidden;

            if (!document.TryGetSyntaxTree(out var tree))
            {
                return severity;
            }

            foreach (var descriptor in analyzer.SupportedDiagnostics)
            {
                if (severity == DiagnosticSeverity.Error)
                {
                    break;
                }

                if (analyzerOptions.TryGetSeverityFromConfiguration(tree, compilation, descriptor, out var reportDiagnostic))
                {
                    var configuredSeverity = reportDiagnostic.ToSeverity();
                    if (configuredSeverity > severity)
                    {
                        severity = configuredSeverity;
                    }

                    continue;
                }

                if (TryGetSeverityFromCodeStyleOption(descriptor, compilation, options, out var codeStyleSeverity))
                {
                    if (codeStyleSeverity > severity)
                    {
                        severity = codeStyleSeverity;
                    }

                    continue;
                }

                if (descriptor.DefaultSeverity > severity)
                {
                    severity = descriptor.DefaultSeverity;
                }
            }

            return severity;

            static bool TryGetSeverityFromCodeStyleOption(
                DiagnosticDescriptor descriptor,
                Compilation compilation,
                OptionSet options,
                out DiagnosticSeverity severity)
            {
                severity = DiagnosticSeverity.Hidden;

                var parameters = new object?[] { descriptor.Id, compilation.Language, null };
                var result = (bool)(TryGetMappedOptionsMethod.Invoke(null, parameters) ?? false);

                if (!result)
                {
                    return false;
                }

                var codeStyleOptions = (IEnumerable)parameters[2]!;
                foreach (var codeStyleOptionObj in codeStyleOptions)
                {
                    var codeStyleOption = (IOption)codeStyleOptionObj!;
                    var option = options.GetOption(new OptionKey(codeStyleOption, codeStyleOption.IsPerLanguage ? compilation.Language : null));
                    if (option is null)
                    {
                        continue;
                    }

                    var notificationProperty = option.GetType().GetProperty("Notification");
                    if (notificationProperty is null)
                    {
                        continue;
                    }

                    var notification = notificationProperty.GetValue(option);
                    var reportDiagnosticValue = notification?.GetType().GetProperty("Severity")?.GetValue(notification);
                    if (reportDiagnosticValue is null)
                    {
                        continue;
                    }

                    var codeStyleSeverity = ToSeverity((ReportDiagnostic)reportDiagnosticValue);
                    if (codeStyleSeverity > severity)
                    {
                        severity = codeStyleSeverity;
                    }
                }

                return true;
            }
        }
    }
}
