﻿// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageService
{
    internal abstract partial class AbstractStructuralTypeDisplayService : IStructuralTypeDisplayService
    {
        protected static readonly SymbolDisplayFormat s_minimalWithoutExpandedTuples = SymbolDisplayFormat.MinimallyQualifiedFormat.AddMiscellaneousOptions(
            SymbolDisplayMiscellaneousOptions.CollapseTupleTypes);

        private static readonly SymbolDisplayFormat s_delegateDisplay =
            s_minimalWithoutExpandedTuples.WithMemberOptions(s_minimalWithoutExpandedTuples.MemberOptions & ~SymbolDisplayMemberOptions.IncludeContainingType);

        protected abstract ISyntaxFacts SyntaxFactsService { get; }
        protected abstract ImmutableArray<SymbolDisplayPart> GetNormalAnonymousTypeParts(INamedTypeSymbol anonymousType, SemanticModel semanticModel, int position);

        public ImmutableArray<SymbolDisplayPart> GetAnonymousTypeParts(INamedTypeSymbol anonymousType, SemanticModel semanticModel, int position)
            => anonymousType.IsAnonymousDelegateType()
                ? GetDelegateAnonymousTypeParts(anonymousType, semanticModel, position)
                : GetNormalAnonymousTypeParts(anonymousType, semanticModel, position);

        private ImmutableArray<SymbolDisplayPart> GetDelegateAnonymousTypeParts(
            INamedTypeSymbol anonymousType,
            SemanticModel semanticModel,
            int position)
        {
            using var _ = ArrayBuilder<SymbolDisplayPart>.GetInstance(out var parts);

            var invokeMethod = anonymousType.DelegateInvokeMethod ?? throw ExceptionUtilities.Unreachable();

            parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Keyword, symbol: null,
                SyntaxFactsService.GetText(SyntaxFactsService.SyntaxKinds.DelegateKeyword)));
            parts.AddRange(Space());
            parts.AddRange(MassageDelegateParts(invokeMethod, invokeMethod.ToMinimalDisplayParts(
                semanticModel, position, s_delegateDisplay)));

            return parts.ToImmutable();
        }

        private static ImmutableArray<SymbolDisplayPart> MassageDelegateParts(
            IMethodSymbol invokeMethod,
            ImmutableArray<SymbolDisplayPart> parts)
        {
            using var _ = ArrayBuilder<SymbolDisplayPart>.GetInstance(out var result);

            // Ugly hack.  Remove the "Invoke" name the compiler layer adds to the parts.
            foreach (var part in parts)
            {
                if (!Equals(invokeMethod, part.Symbol))
                    result.Add(part);
            }

            return result.ToImmutable();
        }

        public StructuralTypeDisplayInfo GetTypeDisplayInfo(
            ISymbol orderSymbol,
            ImmutableArray<INamedTypeSymbol> directStructuralTypeReferences,
            SemanticModel semanticModel,
            int position)
        {
            if (directStructuralTypeReferences.Length == 0)
            {
                return new StructuralTypeDisplayInfo(
                    SpecializedCollections.EmptyDictionary<INamedTypeSymbol, string>(),
                    SpecializedCollections.EmptyList<SymbolDisplayPart>());
            }

            var transitiveStructuralTypeReferences = GetTransitiveStructuralTypeReferences(directStructuralTypeReferences);
            transitiveStructuralTypeReferences = OrderStructuralTypes(transitiveStructuralTypeReferences, orderSymbol);

            IList<SymbolDisplayPart> typeParts = new List<SymbolDisplayPart>();

            if (transitiveStructuralTypeReferences.Length > 0)
            {
                typeParts.Add(PlainText(FeaturesResources.Types_colon));
                typeParts.AddRange(LineBreak());
            }

            for (var i = 0; i < transitiveStructuralTypeReferences.Length; i++)
            {
                if (i != 0)
                {
                    typeParts.AddRange(LineBreak());
                }

                var structuralType = transitiveStructuralTypeReferences[i];
                typeParts.AddRange(Space(count: 4));

                var kind = structuralType.GetSymbolDisplayPartKind();

                typeParts.Add(Part(kind, structuralType, structuralType.Name));
                typeParts.AddRange(Space());
                typeParts.Add(PlainText(FeaturesResources.is_));
                typeParts.AddRange(Space());

                if (structuralType.IsValueType)
                {
                    typeParts.AddRange(structuralType.ToMinimalDisplayParts(semanticModel, position));
                }
                else
                {
                    typeParts.AddRange(GetAnonymousTypeParts(structuralType, semanticModel, position));
                }
            }

            // Finally, assign a name to all the anonymous types.
            var structuralTypeToName = GenerateStructuralTypeNames(transitiveStructuralTypeReferences);
            typeParts = StructuralTypeDisplayInfo.ReplaceStructuralTypes(
                typeParts, structuralTypeToName, semanticModel, position);

            return new StructuralTypeDisplayInfo(structuralTypeToName, typeParts);
        }

        private static Dictionary<INamedTypeSymbol, string> GenerateStructuralTypeNames(
            IList<INamedTypeSymbol> anonymousTypes)
        {
            var current = 0;
            var anonymousTypeToName = new Dictionary<INamedTypeSymbol, string>();
            foreach (var type in anonymousTypes)
            {
                anonymousTypeToName[type] = GenerateStructuralTypeName(current);
                current++;
            }

            return anonymousTypeToName;
        }

        private static string GenerateStructuralTypeName(int current)
        {
            var c = (char)('a' + current);
            if (c is >= 'a' and <= 'z')
            {
                return "'" + c.ToString();
            }

            return "'" + current.ToString();
        }

        private static ImmutableArray<INamedTypeSymbol> OrderStructuralTypes(
            ImmutableArray<INamedTypeSymbol> structuralTypes,
            ISymbol symbol)
        {
            if (symbol is IMethodSymbol method)
            {
                return structuralTypes.OrderBy(
                    (n1, n2) =>
                    {
                        var index1 = method.TypeArguments.IndexOf(n1);
                        var index2 = method.TypeArguments.IndexOf(n2);
                        index1 = index1 < 0 ? int.MaxValue : index1;
                        index2 = index2 < 0 ? int.MaxValue : index2;

                        return index1 - index2;
                    }).ToImmutableArray();
            }
            else if (symbol is IPropertySymbol property)
            {
                return structuralTypes.OrderBy(
                    (n1, n2) =>
                    {
                        if (n1.Equals(property.ContainingType) && !n2.Equals(property.ContainingType))
                        {
                            return -1;
                        }
                        else if (!n1.Equals(property.ContainingType) && n2.Equals(property.ContainingType))
                        {
                            return 1;
                        }
                        else
                        {
                            return 0;
                        }
                    }).ToImmutableArray();
            }

            return structuralTypes;
        }

        private static ImmutableArray<INamedTypeSymbol> GetTransitiveStructuralTypeReferences(
            ImmutableArray<INamedTypeSymbol> structuralTypes)
        {
            var transitiveReferences = new Dictionary<INamedTypeSymbol, (int order, int count)>();
            var visitor = new StructuralTypeCollectorVisitor(transitiveReferences);

            foreach (var type in structuralTypes)
                type.Accept(visitor);

            // If we have at least one tuple that showed up multiple times, then move *all* tuples to the 'Types:'
            // section to clean up the display.
            var hasAtLeastOneTupleWhichAppearsMultipleTimes = transitiveReferences.Any(kvp => kvp.Key.IsTupleType && kvp.Value.count >= 2);

            using var _ = ArrayBuilder<INamedTypeSymbol>.GetInstance(out var result);

            foreach (var (namedType, _) in transitiveReferences.OrderBy(kvp => kvp.Value.order))
            {
                if (namedType.IsTupleType && !hasAtLeastOneTupleWhichAppearsMultipleTimes)
                    continue;

                result.Add(namedType);
            }

            return result.ToImmutable();
        }

        protected static IEnumerable<SymbolDisplayPart> LineBreak(int count = 1)
        {
            for (var i = 0; i < count; i++)
            {
                yield return new SymbolDisplayPart(SymbolDisplayPartKind.LineBreak, null, "\r\n");
            }
        }

        protected static SymbolDisplayPart PlainText(string text)
            => Part(SymbolDisplayPartKind.Text, text);

        private static SymbolDisplayPart Part(SymbolDisplayPartKind kind, string text)
            => Part(kind, null, text);

        private static SymbolDisplayPart Part(SymbolDisplayPartKind kind, ISymbol? symbol, string text)
            => new(kind, symbol, text);

        protected static IEnumerable<SymbolDisplayPart> Space(int count = 1)
        {
            for (var i = 0; i < count; i++)
            {
                yield return new SymbolDisplayPart(SymbolDisplayPartKind.Space, null, " ");
            }
        }

        protected static SymbolDisplayPart Punctuation(string text)
            => Part(SymbolDisplayPartKind.Punctuation, text);

        protected static SymbolDisplayPart Keyword(string text)
            => Part(SymbolDisplayPartKind.Keyword, text);
    }
}
