// 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.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.CodeGeneration
{
    internal class CSharpDeclarationComparer : IComparer<SyntaxNode>
    {
        private static readonly Dictionary<SyntaxKind, int> s_kindPrecedenceMap = new(SyntaxFacts.EqualityComparer)
        {
            { SyntaxKind.FieldDeclaration, 0 },
            { SyntaxKind.ConstructorDeclaration, 1 },
            { SyntaxKind.DestructorDeclaration, 2 },
            { SyntaxKind.IndexerDeclaration, 3 },
            { SyntaxKind.PropertyDeclaration, 4 },
            { SyntaxKind.EventFieldDeclaration, 5 },
            { SyntaxKind.EventDeclaration, 6 },
            { SyntaxKind.MethodDeclaration, 7 },
            { SyntaxKind.OperatorDeclaration, 8 },
            { SyntaxKind.ConversionOperatorDeclaration, 9 },
            { SyntaxKind.EnumDeclaration, 10 },
            { SyntaxKind.InterfaceDeclaration, 11 },
            { SyntaxKind.StructDeclaration, 12 },
            { SyntaxKind.ClassDeclaration, 13 },
            { SyntaxKind.RecordDeclaration, 14 },
            { SyntaxKind.RecordStructDeclaration, 15 },
            { SyntaxKind.DelegateDeclaration, 16 }
        };

        private static readonly Dictionary<SyntaxKind, int> s_operatorPrecedenceMap = new(SyntaxFacts.EqualityComparer)
        {
            { SyntaxKind.PlusToken, 0 },
            { SyntaxKind.MinusToken, 1 },
            { SyntaxKind.ExclamationToken, 2 },
            { SyntaxKind.TildeToken, 3 },
            { SyntaxKind.PlusPlusToken, 4 },
            { SyntaxKind.MinusMinusToken, 5 },
            { SyntaxKind.AsteriskToken, 6 },
            { SyntaxKind.SlashToken, 7 },
            { SyntaxKind.PercentToken, 8 },
            { SyntaxKind.AmpersandToken, 9 },
            { SyntaxKind.BarToken, 10 },
            { SyntaxKind.CaretToken, 11 },
            { SyntaxKind.LessThanLessThanToken, 12 },
            { SyntaxKind.GreaterThanGreaterThanToken, 13 },
            { SyntaxKind.EqualsEqualsToken, 14 },
            { SyntaxKind.ExclamationEqualsToken, 15 },
            { SyntaxKind.LessThanToken, 16 },
            { SyntaxKind.GreaterThanToken, 17 },
            { SyntaxKind.LessThanEqualsToken, 18 },
            { SyntaxKind.GreaterThanEqualsToken, 19 },
            { SyntaxKind.TrueKeyword, 20 },
            { SyntaxKind.FalseKeyword, 21 },
            { SyntaxKind.GreaterThanGreaterThanGreaterThanToken, 22 },
        };

        public static readonly CSharpDeclarationComparer WithNamesInstance = new(includeName: true);
        public static readonly CSharpDeclarationComparer WithoutNamesInstance = new(includeName: false);

        private readonly bool _includeName;

        private CSharpDeclarationComparer(bool includeName)
            => _includeName = includeName;

        public int Compare(SyntaxNode? x, SyntaxNode? y)
        {
            if (ReferenceEquals(x, y))
            {
                return 0;
            }

            if (x is null)
            {
                return -1;
            }

            if (y is null)
            {
                return 1;
            }

            if (x.Kind() != y.Kind())
            {
                if (!s_kindPrecedenceMap.TryGetValue(x.Kind(), out var xPrecedence) ||
                    !s_kindPrecedenceMap.TryGetValue(y.Kind(), out var yPrecedence))
                {
                    // The containing declaration is malformed and contains a node kind we did not expect.
                    // Ignore comparisons with those unexpected nodes and sort them to the end of the declaration.
                    return 1;
                }

                return xPrecedence < yPrecedence ? -1 : 1;
            }

            switch (x.Kind())
            {
                case SyntaxKind.DelegateDeclaration:
                    return Compare((DelegateDeclarationSyntax)x, (DelegateDeclarationSyntax)y);

                case SyntaxKind.FieldDeclaration:
                case SyntaxKind.EventFieldDeclaration:
                    return Compare((BaseFieldDeclarationSyntax)x, (BaseFieldDeclarationSyntax)y);

                case SyntaxKind.ConstructorDeclaration:
                    return Compare((ConstructorDeclarationSyntax)x, (ConstructorDeclarationSyntax)y);

                case SyntaxKind.DestructorDeclaration:
                    // All destructors are equal since there can only be one per named type
                    return 0;

                case SyntaxKind.MethodDeclaration:
                    return Compare((MethodDeclarationSyntax)x, (MethodDeclarationSyntax)y);

                case SyntaxKind.OperatorDeclaration:
                    return Compare((OperatorDeclarationSyntax)x, (OperatorDeclarationSyntax)y);

                case SyntaxKind.EventDeclaration:
                    return Compare((EventDeclarationSyntax)x, (EventDeclarationSyntax)y);

                case SyntaxKind.IndexerDeclaration:
                    return Compare((IndexerDeclarationSyntax)x, (IndexerDeclarationSyntax)y);

                case SyntaxKind.PropertyDeclaration:
                    return Compare((PropertyDeclarationSyntax)x, (PropertyDeclarationSyntax)y);

                case SyntaxKind.EnumDeclaration:
                    return Compare((EnumDeclarationSyntax)x, (EnumDeclarationSyntax)y);

                case SyntaxKind.InterfaceDeclaration:
                case SyntaxKind.StructDeclaration:
                case SyntaxKind.RecordStructDeclaration:
                case SyntaxKind.ClassDeclaration:
                case SyntaxKind.RecordDeclaration:
                    return Compare((BaseTypeDeclarationSyntax)x, (BaseTypeDeclarationSyntax)y);

                case SyntaxKind.ConversionOperatorDeclaration:
                    return Compare((ConversionOperatorDeclarationSyntax)x, (ConversionOperatorDeclarationSyntax)y);

                case SyntaxKind.IncompleteMember:
                    // Since these are incomplete members they are considered to be equal
                    return 0;
                case SyntaxKind.GlobalStatement:
                    // for REPL, don't mess with order, just put new one at the end.
                    return 1;
                default:
                    throw ExceptionUtilities.UnexpectedValue(x.Kind());
            }
        }

        private int Compare(DelegateDeclarationSyntax x, DelegateDeclarationSyntax y)
        {
            if (EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out var result))
            {
                if (_includeName)
                {
                    EqualIdentifierName(x.Identifier, y.Identifier, out result);
                }
            }

            return result;
        }

        private int Compare(BaseFieldDeclarationSyntax x, BaseFieldDeclarationSyntax y)
        {
            if (EqualConstness(x.Modifiers, y.Modifiers, out var result) &&
                EqualStaticness(x.Modifiers, y.Modifiers, out result) &&
                EqualReadOnlyness(x.Modifiers, y.Modifiers, out result) &&
                EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
            {
                if (_includeName)
                {
                    EqualIdentifierName(
                        x.Declaration.Variables.First().Identifier,
                        y.Declaration.Variables.First().Identifier,
                        out result);
                }
            }

            return result;
        }

        private static int Compare(ConstructorDeclarationSyntax x, ConstructorDeclarationSyntax y)
        {
            if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
                EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
            {
                EqualParameterCount(x.ParameterList, y.ParameterList, out result);
            }

            return result;
        }

        private int Compare(MethodDeclarationSyntax x, MethodDeclarationSyntax y)
        {
            if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
                EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
            {
                if (!_includeName)
                {
                    EqualIdentifierName(x.Identifier, y.Identifier, out result);
                }
            }

            return result;
        }

        private static int Compare(ConversionOperatorDeclarationSyntax x, ConversionOperatorDeclarationSyntax y)
        {
            if (x.ImplicitOrExplicitKeyword.Kind() != y.ImplicitOrExplicitKeyword.Kind())
            {
                return x.ImplicitOrExplicitKeyword.Kind() == SyntaxKind.ImplicitKeyword ? -1 : 1;
            }

            EqualParameterCount(x.ParameterList, y.ParameterList, out var result);

            return result;
        }

        private static int Compare(OperatorDeclarationSyntax x, OperatorDeclarationSyntax y)
        {
            if (EqualOperatorPrecedence(x.OperatorToken, y.OperatorToken, out var result))
            {
                EqualParameterCount(x.ParameterList, y.ParameterList, out result);
            }

            return result;
        }

        private int Compare(EventDeclarationSyntax x, EventDeclarationSyntax y)
        {
            if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
                EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
            {
                if (_includeName)
                {
                    EqualIdentifierName(x.Identifier, y.Identifier, out result);
                }
            }

            return result;
        }

        private static int Compare(IndexerDeclarationSyntax x, IndexerDeclarationSyntax y)
        {
            if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
                EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
            {
                EqualParameterCount(x.ParameterList, y.ParameterList, out result);
            }

            return result;
        }

        private int Compare(PropertyDeclarationSyntax x, PropertyDeclarationSyntax y)
        {
            if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
                EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
            {
                if (_includeName)
                {
                    EqualIdentifierName(x.Identifier, y.Identifier, out result);
                }
            }

            return result;
        }

        private int Compare(EnumDeclarationSyntax x, EnumDeclarationSyntax y)
        {
            if (EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out var result))
            {
                if (_includeName)
                {
                    EqualIdentifierName(x.Identifier, y.Identifier, out result);
                }
            }

            return result;
        }

        private int Compare(BaseTypeDeclarationSyntax x, BaseTypeDeclarationSyntax y)
        {
            if (EqualStaticness(x.Modifiers, y.Modifiers, out var result) &&
                EqualAccessibility(x, x.Modifiers, y, y.Modifiers, out result))
            {
                if (_includeName)
                {
                    EqualIdentifierName(x.Identifier, y.Identifier, out result);
                }
            }

            return result;
        }

        private static bool ContainsToken(SyntaxTokenList list, SyntaxKind kind)
            => list.Contains(token => token.Kind() == kind);

        private enum Accessibility
        {
            Public,
            Protected,
            ProtectedInternal,
            Internal,
            PrivateProtected,
            Private
        }

        private static int GetAccessibilityPrecedence(SyntaxTokenList modifiers, SyntaxNode? parent)
        {
            if (ContainsToken(modifiers, SyntaxKind.PublicKeyword))
            {
                return (int)Accessibility.Public;
            }
            else if (ContainsToken(modifiers, SyntaxKind.ProtectedKeyword))
            {
                if (ContainsToken(modifiers, SyntaxKind.InternalKeyword))
                {
                    return (int)Accessibility.ProtectedInternal;
                }

                if (ContainsToken(modifiers, SyntaxKind.PrivateKeyword))
                {
                    return (int)Accessibility.PrivateProtected;
                }

                return (int)Accessibility.Protected;
            }
            else if (ContainsToken(modifiers, SyntaxKind.InternalKeyword))
            {
                return (int)Accessibility.Internal;
            }
            else if (ContainsToken(modifiers, SyntaxKind.PrivateKeyword))
            {
                return (int)Accessibility.Private;
            }

            // Determine default accessibility: This declaration is internal if we traverse up
            // the syntax tree and don't find a containing named type.
            for (var node = parent; node != null; node = node.Parent)
            {
                if (node.Kind() == SyntaxKind.InterfaceDeclaration)
                {
                    // All interface members are public
                    return (int)Accessibility.Public;
                }
                else if (node.Kind() is SyntaxKind.StructDeclaration or SyntaxKind.ClassDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration)
                {
                    // Members and nested types default to private
                    return (int)Accessibility.Private;
                }
            }

            return (int)Accessibility.Internal;
        }

        private static bool BothHaveModifier(SyntaxTokenList x, SyntaxTokenList y, SyntaxKind modifierKind, out int comparisonResult)
        {
            var xHasModifier = ContainsToken(x, modifierKind);
            var yHasModifier = ContainsToken(y, modifierKind);

            if (xHasModifier == yHasModifier)
            {
                comparisonResult = 0;
                return true;
            }

            comparisonResult = xHasModifier ? -1 : 1;
            return false;
        }

        private static bool EqualStaticness(SyntaxTokenList x, SyntaxTokenList y, out int comparisonResult)
            => BothHaveModifier(x, y, SyntaxKind.StaticKeyword, out comparisonResult);

        private static bool EqualConstness(SyntaxTokenList x, SyntaxTokenList y, out int comparisonResult)
            => BothHaveModifier(x, y, SyntaxKind.ConstKeyword, out comparisonResult);

        private static bool EqualReadOnlyness(SyntaxTokenList x, SyntaxTokenList y, out int comparisonResult)
            => BothHaveModifier(x, y, SyntaxKind.ReadOnlyKeyword, out comparisonResult);

        private static bool EqualAccessibility(SyntaxNode x, SyntaxTokenList xModifiers, SyntaxNode y, SyntaxTokenList yModifiers, out int comparisonResult)
        {
            var xAccessibility = GetAccessibilityPrecedence(xModifiers, x.Parent ?? y.Parent);
            var yAccessibility = GetAccessibilityPrecedence(yModifiers, y.Parent ?? x.Parent);

            comparisonResult = xAccessibility - yAccessibility;
            return comparisonResult == 0;
        }

        private static bool EqualIdentifierName(SyntaxToken x, SyntaxToken y, out int comparisonResult)
        {
            comparisonResult = string.Compare(x.ValueText, y.ValueText, StringComparison.OrdinalIgnoreCase);
            return comparisonResult == 0;
        }

        private static bool EqualOperatorPrecedence(SyntaxToken x, SyntaxToken y, out int comparisonResult)
        {
            s_operatorPrecedenceMap.TryGetValue(x.Kind(), out var xPrecedence);
            s_operatorPrecedenceMap.TryGetValue(y.Kind(), out var yPrecedence);

            comparisonResult = xPrecedence - yPrecedence;
            return comparisonResult == 0;
        }

        private static bool EqualParameterCount(BaseParameterListSyntax x, BaseParameterListSyntax y, out int comparisonResult)
        {
            var xParameterCount = x.Parameters.Count;
            var yParameterCount = y.Parameters.Count;

            comparisonResult = xParameterCount - yParameterCount;

            return comparisonResult == 0;
        }
    }
}
