// 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.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.CodeQuality;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions
{
    internal abstract class AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer
        : AbstractCodeQualityDiagnosticAnalyzer
    {
        internal const string DocCommentIdKey = nameof(DocCommentIdKey);

        private static readonly LocalizableResourceString s_localizableTitle = new(
           nameof(AnalyzersResources.Invalid_global_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
        private static readonly LocalizableResourceString s_localizableInvalidScopeMessage = new(
            nameof(AnalyzersResources.Invalid_scope_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
        private static readonly LocalizableResourceString s_localizableInvalidOrMissingTargetMessage = new(
            nameof(AnalyzersResources.Invalid_or_missing_target_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));

        private static readonly DiagnosticDescriptor s_invalidScopeDescriptor = CreateDescriptor(
            IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId,
            EnforceOnBuildValues.InvalidSuppressMessageAttribute,
            s_localizableTitle, s_localizableInvalidScopeMessage, isUnnecessary: true);
        private static readonly DiagnosticDescriptor s_invalidOrMissingTargetDescriptor = CreateDescriptor(
            IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId,
            EnforceOnBuildValues.InvalidSuppressMessageAttribute,
            s_localizableTitle, s_localizableInvalidOrMissingTargetMessage, isUnnecessary: true);

        private static readonly LocalizableResourceString s_localizableLegacyFormatTitle = new(
           nameof(AnalyzersResources.Avoid_legacy_format_target_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
        private static readonly LocalizableResourceString s_localizableLegacyFormatMessage = new(
            nameof(AnalyzersResources.Avoid_legacy_format_target_0_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
        internal static readonly DiagnosticDescriptor LegacyFormatTargetDescriptor = CreateDescriptor(
            IDEDiagnosticIds.LegacyFormatSuppressMessageAttributeDiagnosticId,
            EnforceOnBuildValues.LegacyFormatSuppressMessageAttribute,
            s_localizableLegacyFormatTitle, s_localizableLegacyFormatMessage, isUnnecessary: false);

        protected AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer()
            : base(ImmutableArray.Create(s_invalidScopeDescriptor, s_invalidOrMissingTargetDescriptor, LegacyFormatTargetDescriptor), GeneratedCodeAnalysisFlags.None)
        {
        }

        protected abstract void RegisterAttributeSyntaxAction(CompilationStartAnalysisContext context, CompilationAnalyzer compilationAnalyzer);
        public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis;

        protected sealed override void InitializeWorker(AnalysisContext context)
        {
            context.RegisterCompilationStartAction(context =>
            {
                var suppressMessageAttributeType = context.Compilation.SuppressMessageAttributeType();
                if (suppressMessageAttributeType == null)
                {
                    return;
                }

                RegisterAttributeSyntaxAction(context, new CompilationAnalyzer(context.Compilation, suppressMessageAttributeType));
            });
        }

        protected sealed class CompilationAnalyzer
        {
            private readonly SuppressMessageAttributeState _state;

            public CompilationAnalyzer(Compilation compilation, INamedTypeSymbol suppressMessageAttributeType)
            {
                _state = new SuppressMessageAttributeState(compilation, suppressMessageAttributeType);
            }

            public void AnalyzeAssemblyOrModuleAttribute(SyntaxNode attributeSyntax, SemanticModel model, Action<Diagnostic> reportDiagnostic, CancellationToken cancellationToken)
            {
                if (!_state.IsSuppressMessageAttributeWithNamedArguments(attributeSyntax, model, cancellationToken, out var namedAttributeArguments))
                {
                    return;
                }

                if (!SuppressMessageAttributeState.HasValidScope(namedAttributeArguments, out var targetScope))
                {
                    reportDiagnostic(Diagnostic.Create(s_invalidScopeDescriptor, attributeSyntax.GetLocation()));
                    return;
                }

                if (!_state.HasValidTarget(namedAttributeArguments, targetScope, out var targetHasDocCommentIdFormat,
                        out var targetSymbolString, out var targetValueOperation, out var resolvedSymbols))
                {
                    reportDiagnostic(Diagnostic.Create(s_invalidOrMissingTargetDescriptor, attributeSyntax.GetLocation()));
                    return;
                }

                // We want to flag valid target which uses legacy format to update to Roslyn based DocCommentId format.
                if (resolvedSymbols.Length > 0 && !targetHasDocCommentIdFormat)
                {
                    RoslynDebug.Assert(!string.IsNullOrEmpty(targetSymbolString));
                    RoslynDebug.Assert(targetValueOperation != null);

                    var properties = ImmutableDictionary<string, string?>.Empty;
                    if (resolvedSymbols.Length == 1)
                    {
                        // We provide a code fix for the case where the target resolved to a single symbol.
                        var docCommentId = DocumentationCommentId.CreateDeclarationId(resolvedSymbols[0]);
                        if (!string.IsNullOrEmpty(docCommentId))
                        {
                            // Suppression target has an optional "~" prefix to distinguish it from legacy FxCop suppressions.
                            // IDE suppression code fixes emit this prefix, so we we also add this prefix to new suppression target string.
                            properties = properties.Add(DocCommentIdKey, "~" + docCommentId);
                        }
                    }

                    reportDiagnostic(Diagnostic.Create(LegacyFormatTargetDescriptor, targetValueOperation.Syntax.GetLocation(), properties!, targetSymbolString));
                    return;
                }
            }
        }
    }
}
