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

using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers.Fixers
{
    [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared]
    public sealed partial class DefineDiagnosticDescriptorArgumentsCorrectlyFix : CodeFixProvider
    {
        private const string SourceDocumentEquivalenceKeySuffix = nameof(SourceDocumentEquivalenceKeySuffix);
        private const string AdditionalDocumentEquivalenceKeySuffix = nameof(AdditionalDocumentEquivalenceKeySuffix);

        public override ImmutableArray<string> FixableDiagnosticIds =>
            ImmutableArray.Create(DiagnosticIds.DefineDiagnosticTitleCorrectlyRuleId,
                                  DiagnosticIds.DefineDiagnosticMessageCorrectlyRuleId,
                                  DiagnosticIds.DefineDiagnosticDescriptionCorrectlyRuleId);

        public override FixAllProvider GetFixAllProvider() => CustomFixAllProvider.Instance;

        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
            var node = root.FindNode(context.Span, getInnermostNodeForTie: true);
            if (node is null)
            {
                return;
            }

            var additionalDocuments = context.Document.Project.AdditionalDocuments.ToImmutableArray();
            var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
            foreach (var diagnostic in context.Diagnostics)
            {
                if (!TryGetFixInfo(diagnostic, root, semanticModel, additionalDocuments,
                        context.CancellationToken, out var fixInfo))
                {
                    continue;
                }

                var codeFixTitle = diagnostic.Id switch
                {
                    DiagnosticIds.DefineDiagnosticTitleCorrectlyRuleId => CodeAnalysisDiagnosticsResources.DefineDiagnosticTitleCorrectlyTitle,
                    DiagnosticIds.DefineDiagnosticMessageCorrectlyRuleId => CodeAnalysisDiagnosticsResources.DefineDiagnosticMessageCorrectlyTitle,
                    DiagnosticIds.DefineDiagnosticDescriptionCorrectlyRuleId => CodeAnalysisDiagnosticsResources.DefineDiagnosticDescriptionCorrectlyTitle,
                    _ => throw new InvalidOperationException()
                };

                var equivalenceKeySuffix = fixInfo.Value.AdditionalDocumentToFix != null ? AdditionalDocumentEquivalenceKeySuffix : SourceDocumentEquivalenceKeySuffix;
                var equivalenceKey = codeFixTitle + equivalenceKeySuffix;

                var codeAction = new MyCodeAction(
                   codeFixTitle,
                   ct => ApplyFixAsync(context.Document, root, fixInfo.Value, ct),
                   equivalenceKey);
                context.RegisterCodeFix(codeAction, diagnostic);
            }
        }

#pragma warning disable CA1815 // Override equals and operator equals on value types
        private readonly struct FixInfo
#pragma warning restore CA1815 // Override equals and operator equals on value types
        {
            public string FixValue { get; }

            public FixInfo(string fixValue, ILiteralOperation sourceLiteralAtLocationToFix)
            {
                FixValue = fixValue;
                SourceLiteralAtLocationToFix = sourceLiteralAtLocationToFix;
                AdditionalDocumentToFix = null;
                AdditionalDocumentSpanToFix = null;
            }

            public FixInfo(string fixValue, TextDocument additionalDocumentToFix, TextSpan additionalDocumentSpanToFix)
            {
                FixValue = fixValue;
                AdditionalDocumentToFix = additionalDocumentToFix;
                AdditionalDocumentSpanToFix = additionalDocumentSpanToFix;
                SourceLiteralAtLocationToFix = null;
            }

            public ILiteralOperation? SourceLiteralAtLocationToFix { get; }
            public TextDocument? AdditionalDocumentToFix { get; }
            public TextSpan? AdditionalDocumentSpanToFix { get; }
        }

        private static bool TryGetFixInfo(
            Diagnostic diagnostic,
            SyntaxNode root,
            SemanticModel model,
            ImmutableArray<TextDocument> additionalDocuments,
            CancellationToken cancellationToken,
            [NotNullWhen(returnValue: true)] out FixInfo? fixInfo)
        {
            fixInfo = null;

            if (!TryGetFixValue(diagnostic, out var fixValue))
            {
                return false;
            }

            if (diagnostic.AdditionalLocations.Count == 1)
            {
                var locationToFix = diagnostic.AdditionalLocations[0];
                if (locationToFix.IsInSource &&
                    root.FindNode(locationToFix.SourceSpan, getInnermostNodeForTie: true) is { } fixNode &&
                    model.GetOperation(fixNode, cancellationToken) is ILiteralOperation literal &&
                    literal.ConstantValue.HasValue &&
                    literal.ConstantValue.Value is string)
                {
                    fixInfo = new FixInfo(fixValue, literal);
                    return true;
                }

                return false;
            }

            return TryGetAdditionalDocumentFixInfo(diagnostic, fixValue, additionalDocuments, out fixInfo);
        }

        private static bool TryGetFixValue(Diagnostic diagnostic, [NotNullWhen(returnValue: true)] out string? fixValue)
            => diagnostic.Properties.TryGetValue(DiagnosticDescriptorCreationAnalyzer.DefineDescriptorArgumentCorrectlyFixValue, out fixValue) &&
               !string.IsNullOrEmpty(fixValue);

        private static bool TryGetAdditionalDocumentFixInfo(
            Diagnostic diagnostic,
            string fixValue,
            ImmutableArray<TextDocument> additionalDocuments,
            [NotNullWhen(returnValue: true)] out FixInfo? fixInfo)
        {
            if (DiagnosticDescriptorCreationAnalyzer.TryGetAdditionalDocumentLocationInfo(diagnostic, out var path, out var fixSpan) &&
                additionalDocuments.FirstOrDefault(a => string.Equals(a.FilePath, path, StringComparison.Ordinal)) is { } additionalDocument)
            {
                fixInfo = new FixInfo(fixValue, additionalDocument, fixSpan.Value);
                return true;
            }

            fixInfo = null;
            return false;
        }

        private static async Task<Solution> ApplyFixAsync(Document document, SyntaxNode root, FixInfo fixInfo, CancellationToken cancellationToken)
        {
            if (fixInfo.SourceLiteralAtLocationToFix is { } literal)
            {
                RoslynDebug.Assert(literal.ConstantValue.HasValue && literal.ConstantValue.Value is string);

                var generator = SyntaxGenerator.GetGenerator(document);
                var newLiteral = generator.LiteralExpression(fixInfo.FixValue).WithTriviaFrom(literal.Syntax);
                var newRoot = root.ReplaceNode(literal.Syntax, newLiteral);
                return document.WithSyntaxRoot(newRoot).Project.Solution;
            }
            else
            {
                RoslynDebug.Assert(fixInfo.AdditionalDocumentToFix != null);
                RoslynDebug.Assert(fixInfo.AdditionalDocumentSpanToFix != null);

                var text = await fixInfo.AdditionalDocumentToFix.GetTextAsync(cancellationToken).ConfigureAwait(false);
                var textChange = new TextChange(fixInfo.AdditionalDocumentSpanToFix.Value, fixInfo.FixValue);
                var newText = text.WithChanges(textChange);
                return document.Project.Solution.WithAdditionalDocumentText(fixInfo.AdditionalDocumentToFix.Id, newText);
            }
        }

        // Needed for Telemetry (https://github.com/dotnet/roslyn-analyzers/issues/192)
        private sealed class MyCodeAction : SolutionChangeAction
        {
            public MyCodeAction(string title, Func<CancellationToken, Task<Solution>> createChangedSolution, string equivalenceKey)
                : base(title, createChangedSolution, equivalenceKey)
            {
            }
        }
    }
}
