// 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.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.AliasAmbiguousType
{
    internal abstract class AbstractAliasAmbiguousTypeCodeFixProvider : CodeFixProvider
    {
        protected abstract string GetTextPreviewOfChange(string aliasName, ITypeSymbol typeSymbol);

        public override FixAllProvider? GetFixAllProvider() => null;

        public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var cancellationToken = context.CancellationToken;
            var document = context.Document;
            var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
            var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            // Innermost: We are looking for an IdentifierName. IdentifierName is sometimes at the same span as its parent (e.g. SimpleBaseTypeSyntax).
            var diagnosticNode = root.FindNode(context.Span, getInnermostNodeForTie: true);
            if (!syntaxFacts.IsIdentifierName(diagnosticNode))
                return;

            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var symbolInfo = semanticModel.GetSymbolInfo(diagnosticNode, cancellationToken);
            if (!SymbolCandidatesContainsSupportedSymbols(symbolInfo))
                return;

            var addImportService = document.GetRequiredLanguageService<IAddImportsService>();
            var syntaxGenerator = document.GetRequiredLanguageService<SyntaxGenerator>();
            var compilation = semanticModel.Compilation;

            var placementOption = await document.GetAddImportPlacementOptionsAsync(addImportService, context.GetOptionsProvider(), cancellationToken).ConfigureAwait(false);

            using var _ = ArrayBuilder<CodeAction>.GetInstance(out var actions);
            foreach (var symbol in Sort(symbolInfo.CandidateSymbols.Cast<ITypeSymbol>(), placementOption.PlaceSystemNamespaceFirst))
            {
                var typeName = symbol.Name;
                var title = GetTextPreviewOfChange(typeName, symbol);

                actions.Add(CodeAction.Create(
                    title,
                    cancellationToken =>
                    {
                        var aliasDirective = syntaxGenerator.AliasImportDeclaration(typeName, symbol);
                        var newRoot = addImportService.AddImport(compilation, root, diagnosticNode, aliasDirective, syntaxGenerator, placementOption, cancellationToken);
                        return Task.FromResult(document.WithSyntaxRoot(newRoot));
                    },
                    title));
            }

            context.RegisterCodeFix(
                CodeAction.Create(
                    string.Format(CodeFixesResources.Alias_ambiguous_type_0, diagnosticNode.ToString()),
                    actions.ToImmutable(),
                    isInlinable: true),
                context.Diagnostics.First());
        }

        private static IEnumerable<ITypeSymbol> Sort(IEnumerable<ITypeSymbol> types, bool sortSystemFirst)
        {
            // get all the name portions of the fully-qualified-names of the types in 'types'.
            // cache these in this local dictionary so we only have to compute them once.
            var typeToNameSegments = new Dictionary<ITypeSymbol, ImmutableArray<string>>();

            return types.OrderBy((t1, t2) =>
            {
                var t1NameSegments = GetNameSegments(t1);
                var t2NameSegments = GetNameSegments(t2);

                // compare all the name segments the two types have in common.
                for (int i = 0, n = Math.Min(t1NameSegments.Length, t2NameSegments.Length); i < n; i++)
                {
                    var t1NameSegment = t1NameSegments[i];
                    var t2NameSegment = t2NameSegments[i];

                    // if we're on the first name segment, ensure we sort 'System' properly if the user
                    // prefers them coming first.
                    var comparer = i == 0 && sortSystemFirst ? SortSystemFirstComparer.Instance : StringComparer.Ordinal;

                    var diff = comparer.Compare(t1NameSegment, t2NameSegment);
                    if (diff != 0)
                        return diff;
                }

                // if all the names matched up to this point, then the type with the shorter number of segments comes first.
                return t1NameSegments.Length - t2NameSegments.Length;
            });

            ImmutableArray<string> GetNameSegments(ITypeSymbol symbol)
            {
                return typeToNameSegments.GetOrAdd(symbol, static symbol =>
                {
                    using var result = TemporaryArray<string>.Empty;

                    for (ISymbol current = symbol; current != null; current = current.ContainingSymbol)
                    {
                        if (string.IsNullOrEmpty(current.Name))
                            break;

                        result.Add(current.Name);
                    }

                    // We walked upwards to get the name segments.  So reverse teh order here so it goes from outer-most to
                    // inner-most names.
                    result.ReverseContents();
                    return result.ToImmutableAndClear();
                });
            }
        }

        private static bool SymbolCandidatesContainsSupportedSymbols(SymbolInfo symbolInfo)
            => symbolInfo.CandidateReason == CandidateReason.Ambiguous &&
               // Arity: Aliases can only name closed constructed types. (See also proposal https://github.com/dotnet/csharplang/issues/1239)
               // Aliasing as a closed constructed type is possible but would require to remove the type arguments from the diagnosed node.
               // It is unlikely that the user wants that and so generic types are not supported.
               symbolInfo.CandidateSymbols.All(symbol => symbol.IsKind(SymbolKind.NamedType) &&
                                                         symbol.GetArity() == 0);

        private sealed class SortSystemFirstComparer : IComparer<string>
        {
            public static readonly IComparer<string> Instance = new SortSystemFirstComparer();

            public int Compare(string? x, string? y)
            {
                var xIsSystem = x == nameof(System);
                var yIsSystem = y == nameof(System);

                if (xIsSystem && yIsSystem)
                    return 0;

                if (xIsSystem)
                    return -1;

                if (yIsSystem)
                    return 1;

                return StringComparer.Ordinal.Compare(x, y);
            }
        }
    }
}
