// 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 Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using System.Diagnostics.CodeAnalysis;

#if CODE_STYLE
using Microsoft.CodeAnalysis.Internal.Editing;
#else
using Microsoft.CodeAnalysis.Editing;
#endif

namespace Microsoft.CodeAnalysis.CSharp.LanguageService
{
    internal class CSharpAccessibilityFacts : IAccessibilityFacts
    {
        public static readonly IAccessibilityFacts Instance = new CSharpAccessibilityFacts();

        private CSharpAccessibilityFacts()
        {
        }

        public bool CanHaveAccessibility(SyntaxNode declaration, bool ignoreDeclarationModifiers = false)
        {
            switch (declaration.Kind())
            {
                case SyntaxKind.ClassDeclaration:
                case SyntaxKind.RecordDeclaration:
                case SyntaxKind.StructDeclaration:
                case SyntaxKind.RecordStructDeclaration:
                case SyntaxKind.InterfaceDeclaration:
                case SyntaxKind.EnumDeclaration:
                case SyntaxKind.DelegateDeclaration:
                    return ignoreDeclarationModifiers || !((MemberDeclarationSyntax)declaration).Modifiers.Any(SyntaxKind.FileKeyword);

                case SyntaxKind.FieldDeclaration:
                case SyntaxKind.EventFieldDeclaration:
                case SyntaxKind.GetAccessorDeclaration:
                case SyntaxKind.SetAccessorDeclaration:
                case SyntaxKind.InitAccessorDeclaration:
                case SyntaxKind.AddAccessorDeclaration:
                case SyntaxKind.RemoveAccessorDeclaration:
                    return true;

                case SyntaxKind.VariableDeclaration:
                case SyntaxKind.VariableDeclarator:
                    var declarationKind = GetDeclarationKind(declaration);
                    return declarationKind is DeclarationKind.Field or DeclarationKind.Event;

                case SyntaxKind.ConstructorDeclaration:
                    // Static constructor can't have accessibility
                    return ignoreDeclarationModifiers || !((ConstructorDeclarationSyntax)declaration).Modifiers.Any(SyntaxKind.StaticKeyword);

                case SyntaxKind.PropertyDeclaration:
                    return ((PropertyDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null;

                case SyntaxKind.IndexerDeclaration:
                    return ((IndexerDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null;

                case SyntaxKind.OperatorDeclaration:
                    return ((OperatorDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null;

                case SyntaxKind.ConversionOperatorDeclaration:
                    return ((ConversionOperatorDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null;

                case SyntaxKind.MethodDeclaration:
                    var method = (MethodDeclarationSyntax)declaration;
                    if (method.ExplicitInterfaceSpecifier != null)
                    {
                        // explicit interface methods can't have accessibility.
                        return false;
                    }

                    if (method.Modifiers.Any(SyntaxKind.PartialKeyword))
                    {
                        // partial methods can't have accessibility modifiers.
                        return false;
                    }

                    return true;

                case SyntaxKind.EventDeclaration:
                    return ((EventDeclarationSyntax)declaration).ExplicitInterfaceSpecifier == null;

                default:
                    return false;
            }
        }

        public Accessibility GetAccessibility(SyntaxNode declaration)
        {
            if (!CanHaveAccessibility(declaration))
                return Accessibility.NotApplicable;

            var modifierTokens = GetModifierTokens(declaration);
            GetAccessibilityAndModifiers(modifierTokens, out var accessibility, out _, out _);
            return accessibility;
        }

        public static void GetAccessibilityAndModifiers(SyntaxTokenList modifierList, out Accessibility accessibility, out DeclarationModifiers modifiers, out bool isDefault)
        {
            accessibility = Accessibility.NotApplicable;
            modifiers = DeclarationModifiers.None;
            isDefault = false;

            foreach (var token in modifierList)
            {
                accessibility = (token.Kind(), accessibility) switch
                {
                    (SyntaxKind.PublicKeyword, _) => Accessibility.Public,

                    (SyntaxKind.PrivateKeyword, Accessibility.Protected) => Accessibility.ProtectedAndInternal,
                    (SyntaxKind.PrivateKeyword, _) => Accessibility.Private,

                    (SyntaxKind.InternalKeyword, Accessibility.Protected) => Accessibility.ProtectedOrInternal,
                    (SyntaxKind.InternalKeyword, _) => Accessibility.Internal,

                    (SyntaxKind.ProtectedKeyword, Accessibility.Private) => Accessibility.ProtectedAndInternal,
                    (SyntaxKind.ProtectedKeyword, Accessibility.Internal) => Accessibility.ProtectedOrInternal,
                    (SyntaxKind.ProtectedKeyword, _) => Accessibility.Protected,

                    _ => accessibility,
                };

                modifiers |= token.Kind() switch
                {
                    SyntaxKind.AbstractKeyword => DeclarationModifiers.Abstract,
                    SyntaxKind.NewKeyword => DeclarationModifiers.New,
                    SyntaxKind.OverrideKeyword => DeclarationModifiers.Override,
                    SyntaxKind.VirtualKeyword => DeclarationModifiers.Virtual,
                    SyntaxKind.StaticKeyword => DeclarationModifiers.Static,
                    SyntaxKind.AsyncKeyword => DeclarationModifiers.Async,
                    SyntaxKind.ConstKeyword => DeclarationModifiers.Const,
                    SyntaxKind.ReadOnlyKeyword => DeclarationModifiers.ReadOnly,
                    SyntaxKind.SealedKeyword => DeclarationModifiers.Sealed,
                    SyntaxKind.UnsafeKeyword => DeclarationModifiers.Unsafe,
                    SyntaxKind.PartialKeyword => DeclarationModifiers.Partial,
                    SyntaxKind.RefKeyword => DeclarationModifiers.Ref,
                    SyntaxKind.VolatileKeyword => DeclarationModifiers.Volatile,
                    SyntaxKind.ExternKeyword => DeclarationModifiers.Extern,
                    SyntaxKind.FileKeyword => DeclarationModifiers.File,
                    SyntaxKind.RequiredKeyword => DeclarationModifiers.Required,
                    _ => DeclarationModifiers.None,
                };

                isDefault |= token.Kind() == SyntaxKind.DefaultKeyword;
            }
        }

        public static DeclarationKind GetDeclarationKind(SyntaxNode declaration)
        {
            switch (declaration.Kind())
            {
                case SyntaxKind.ClassDeclaration:
                case SyntaxKind.RecordDeclaration:
                    return DeclarationKind.Class;
                case SyntaxKind.StructDeclaration:
                case SyntaxKind.RecordStructDeclaration:
                    return DeclarationKind.Struct;
                case SyntaxKind.InterfaceDeclaration:
                    return DeclarationKind.Interface;
                case SyntaxKind.EnumDeclaration:
                    return DeclarationKind.Enum;
                case SyntaxKind.DelegateDeclaration:
                    return DeclarationKind.Delegate;

                case SyntaxKind.MethodDeclaration:
                    return DeclarationKind.Method;
                case SyntaxKind.OperatorDeclaration:
                    return DeclarationKind.Operator;
                case SyntaxKind.ConversionOperatorDeclaration:
                    return DeclarationKind.ConversionOperator;
                case SyntaxKind.ConstructorDeclaration:
                    return DeclarationKind.Constructor;
                case SyntaxKind.DestructorDeclaration:
                    return DeclarationKind.Destructor;

                case SyntaxKind.PropertyDeclaration:
                    return DeclarationKind.Property;
                case SyntaxKind.IndexerDeclaration:
                    return DeclarationKind.Indexer;
                case SyntaxKind.EventDeclaration:
                    return DeclarationKind.CustomEvent;
                case SyntaxKind.EnumMemberDeclaration:
                    return DeclarationKind.EnumMember;
                case SyntaxKind.CompilationUnit:
                    return DeclarationKind.CompilationUnit;
                case SyntaxKind.NamespaceDeclaration:
                case SyntaxKind.FileScopedNamespaceDeclaration:
                    return DeclarationKind.Namespace;
                case SyntaxKind.UsingDirective:
                    return DeclarationKind.NamespaceImport;
                case SyntaxKind.Parameter:
                    return DeclarationKind.Parameter;

                case SyntaxKind.ParenthesizedLambdaExpression:
                case SyntaxKind.SimpleLambdaExpression:
                    return DeclarationKind.LambdaExpression;

                case SyntaxKind.FieldDeclaration:
                    var fd = (FieldDeclarationSyntax)declaration;
                    if (fd.Declaration != null && fd.Declaration.Variables.Count == 1)
                    {
                        // this node is considered the declaration if it contains only one variable.
                        return DeclarationKind.Field;
                    }
                    else
                    {
                        return DeclarationKind.None;
                    }

                case SyntaxKind.EventFieldDeclaration:
                    var ef = (EventFieldDeclarationSyntax)declaration;
                    if (ef.Declaration != null && ef.Declaration.Variables.Count == 1)
                    {
                        // this node is considered the declaration if it contains only one variable.
                        return DeclarationKind.Event;
                    }
                    else
                    {
                        return DeclarationKind.None;
                    }

                case SyntaxKind.LocalDeclarationStatement:
                    var ld = (LocalDeclarationStatementSyntax)declaration;
                    if (ld.Declaration != null && ld.Declaration.Variables.Count == 1)
                    {
                        // this node is considered the declaration if it contains only one variable.
                        return DeclarationKind.Variable;
                    }
                    else
                    {
                        return DeclarationKind.None;
                    }

                case SyntaxKind.VariableDeclaration:
                    {
                        var vd = (VariableDeclarationSyntax)declaration;
                        if (vd.Variables.Count == 1 && vd.Parent == null)
                        {
                            // this node is the declaration if it contains only one variable and has no parent.
                            return DeclarationKind.Variable;
                        }
                        else
                        {
                            return DeclarationKind.None;
                        }
                    }

                case SyntaxKind.VariableDeclarator:
                    {
                        var vd = declaration.Parent as VariableDeclarationSyntax;

                        // this node is considered the declaration if it is one among many, or it has no parent
                        if (vd == null || vd.Variables.Count > 1)
                        {
                            if (ParentIsFieldDeclaration(vd))
                            {
                                return DeclarationKind.Field;
                            }
                            else if (ParentIsEventFieldDeclaration(vd))
                            {
                                return DeclarationKind.Event;
                            }
                            else
                            {
                                return DeclarationKind.Variable;
                            }
                        }

                        break;
                    }

                case SyntaxKind.AttributeList:
                    var list = (AttributeListSyntax)declaration;
                    if (list.Attributes.Count == 1)
                    {
                        return DeclarationKind.Attribute;
                    }

                    break;

                case SyntaxKind.Attribute:
                    if (declaration.Parent is not AttributeListSyntax parentList || parentList.Attributes.Count > 1)
                    {
                        return DeclarationKind.Attribute;
                    }

                    break;

                case SyntaxKind.GetAccessorDeclaration:
                    return DeclarationKind.GetAccessor;
                case SyntaxKind.SetAccessorDeclaration:
                case SyntaxKind.InitAccessorDeclaration:
                    return DeclarationKind.SetAccessor;
                case SyntaxKind.AddAccessorDeclaration:
                    return DeclarationKind.AddAccessor;
                case SyntaxKind.RemoveAccessorDeclaration:
                    return DeclarationKind.RemoveAccessor;
            }

            return DeclarationKind.None;
        }

        public static SyntaxTokenList GetModifierTokens(SyntaxNode declaration)
            => declaration switch
            {
                MemberDeclarationSyntax memberDecl => memberDecl.Modifiers,
                ParameterSyntax parameter => parameter.Modifiers,
                LocalDeclarationStatementSyntax localDecl => localDecl.Modifiers,
                LocalFunctionStatementSyntax localFunc => localFunc.Modifiers,
                AccessorDeclarationSyntax accessor => accessor.Modifiers,
                VariableDeclarationSyntax varDecl => GetModifierTokens(varDecl.GetRequiredParent()),
                VariableDeclaratorSyntax varDecl => GetModifierTokens(varDecl.GetRequiredParent()),
                AnonymousFunctionExpressionSyntax anonymous => anonymous.Modifiers,
                _ => default,
            };

        public static bool ParentIsFieldDeclaration([NotNullWhen(true)] SyntaxNode? node)
            => node?.Parent.IsKind(SyntaxKind.FieldDeclaration) ?? false;

        public static bool ParentIsEventFieldDeclaration([NotNullWhen(true)] SyntaxNode? node)
            => node?.Parent.IsKind(SyntaxKind.EventFieldDeclaration) ?? false;

        public static bool ParentIsLocalDeclarationStatement([NotNullWhen(true)] SyntaxNode? node)
            => node?.Parent.IsKind(SyntaxKind.LocalDeclarationStatement) ?? false;
    }
}
