// 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 System.Threading;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.CodeGeneration.CodeGenerationHelpers;
using static Microsoft.CodeAnalysis.CSharp.CodeGeneration.CSharpCodeGenerationHelpers;

namespace Microsoft.CodeAnalysis.CSharp.CodeGeneration
{
    internal static class NamedTypeGenerator
    {
        public static TypeDeclarationSyntax AddNamedTypeTo(
            ICodeGenerationService service,
            TypeDeclarationSyntax destination,
            INamedTypeSymbol namedType,
            CSharpCodeGenerationContextInfo info,
            IList<bool>? availableIndices,
            CancellationToken cancellationToken)
        {
            var declaration = GenerateNamedTypeDeclaration(service, namedType, GetDestination(destination), info, cancellationToken);
            var members = Insert(destination.Members, declaration, info, availableIndices);

            return AddMembersTo(destination, members, cancellationToken);
        }

        public static BaseNamespaceDeclarationSyntax AddNamedTypeTo(
            ICodeGenerationService service,
            BaseNamespaceDeclarationSyntax destination,
            INamedTypeSymbol namedType,
            CSharpCodeGenerationContextInfo info,
            IList<bool>? availableIndices,
            CancellationToken cancellationToken)
        {
            var declaration = GenerateNamedTypeDeclaration(service, namedType, CodeGenerationDestination.Namespace, info, cancellationToken);
            var members = Insert(destination.Members, declaration, info, availableIndices);
            return ConditionallyAddFormattingAnnotationTo(
                destination.WithMembers(members),
                members);
        }

        public static CompilationUnitSyntax AddNamedTypeTo(
            ICodeGenerationService service,
            CompilationUnitSyntax destination,
            INamedTypeSymbol namedType,
            CSharpCodeGenerationContextInfo info,
            IList<bool>? availableIndices,
            CancellationToken cancellationToken)
        {
            var declaration = GenerateNamedTypeDeclaration(service, namedType, CodeGenerationDestination.CompilationUnit, info, cancellationToken);
            var members = Insert(destination.Members, declaration, info, availableIndices);
            return destination.WithMembers(members);
        }

        public static MemberDeclarationSyntax GenerateNamedTypeDeclaration(
            ICodeGenerationService service,
            INamedTypeSymbol namedType,
            CodeGenerationDestination destination,
            CSharpCodeGenerationContextInfo info,
            CancellationToken cancellationToken)
        {
            var declaration = GetDeclarationSyntaxWithoutMembers(namedType, destination, info);

            // If we are generating members then make sure to exclude properties that cannot be generated.
            // Reason: Calling AddProperty on a propertysymbol that can't be generated (like one with params) causes
            // the getter and setter to get generated instead. Since the list of members is going to include
            // the method symbols for the getter and setter, we don't want to generate them twice.

            var members = GetMembers(namedType).Where(s => s.Kind != SymbolKind.Property || PropertyGenerator.CanBeGenerated((IPropertySymbol)s))
                                               .ToImmutableArray();
            if (namedType.IsRecord)
            {
                declaration = GenerateRecordMembers(service, info, (RecordDeclarationSyntax)declaration, members, cancellationToken);
            }
            else
            {
                // If we're generating a ComImport type, then do not attempt to do any
                // reordering of members.
                if (namedType.IsComImport)
                    info = info.WithContext(info.Context.With(autoInsertionLocation: false, sortMembers: false));

                if (info.Context.GenerateMembers && namedType.TypeKind != TypeKind.Delegate)
                    declaration = service.AddMembers(declaration, members, info, cancellationToken);
            }

            return AddFormatterAndCodeGeneratorAnnotationsTo(ConditionallyAddDocumentationCommentTo(declaration, namedType, info, cancellationToken));
        }

        private static RecordDeclarationSyntax GenerateRecordMembers(
            ICodeGenerationService service,
            CSharpCodeGenerationContextInfo info,
            RecordDeclarationSyntax recordDeclaration,
            ImmutableArray<ISymbol> members,
            CancellationToken cancellationToken)
        {
            if (!info.Context.GenerateMembers)
                members = ImmutableArray<ISymbol>.Empty;

            // For a record, add record parameters if we have a primary constructor.
            var primaryConstructor = members.OfType<IMethodSymbol>().FirstOrDefault(m => CodeGenerationConstructorInfo.GetIsPrimaryConstructor(m));
            if (primaryConstructor != null)
            {
                var parameterList = ParameterGenerator.GenerateParameterList(primaryConstructor.Parameters, isExplicit: false, info);
                recordDeclaration = recordDeclaration.WithParameterList(parameterList);

                // remove the primary constructor from the list of members to generate.
                members = members.Remove(primaryConstructor);

                // remove any fields/properties that were created by the primary constructor
                members = members.WhereAsArray(m => m is not IPropertySymbol and not IFieldSymbol || !primaryConstructor.Parameters.Any(static (p, m) => p.Name == m.Name, m));
            }

            // remove any implicit overrides to generate.
            members = members.WhereAsArray(m => !m.IsImplicitlyDeclared);

            // If there are no members, just make a simple record with no body
            if (members.Length == 0)
                return recordDeclaration.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));

            // Otherwise, give the record a body and add the members to it.
            recordDeclaration = recordDeclaration.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken))
                                                 .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken))
                                                 .WithSemicolonToken(default);
            return service.AddMembers(recordDeclaration, members, info, cancellationToken);
        }

        public static MemberDeclarationSyntax UpdateNamedTypeDeclaration(
            ICodeGenerationService service,
            MemberDeclarationSyntax declaration,
            IList<ISymbol> newMembers,
            CSharpCodeGenerationContextInfo info,
            CancellationToken cancellationToken)
        {
            declaration = RemoveAllMembers(declaration);
            declaration = service.AddMembers(declaration, newMembers, info, cancellationToken);
            return AddFormatterAndCodeGeneratorAnnotationsTo(declaration);
        }

        private static MemberDeclarationSyntax GetDeclarationSyntaxWithoutMembers(
            INamedTypeSymbol namedType,
            CodeGenerationDestination destination,
            CSharpCodeGenerationContextInfo info)
        {
            var reusableDeclarationSyntax = GetReuseableSyntaxNodeForSymbol<MemberDeclarationSyntax>(namedType, info);
            return reusableDeclarationSyntax == null
                ? GetDeclarationSyntaxWithoutMembersWorker(namedType, destination, info)
                : RemoveAllMembers(reusableDeclarationSyntax);
        }

        private static MemberDeclarationSyntax RemoveAllMembers(MemberDeclarationSyntax declaration)
        {
            switch (declaration.Kind())
            {
                case SyntaxKind.EnumDeclaration:
                    return ((EnumDeclarationSyntax)declaration).WithMembers(default);

                case SyntaxKind.StructDeclaration:
                case SyntaxKind.RecordStructDeclaration:
                case SyntaxKind.InterfaceDeclaration:
                case SyntaxKind.ClassDeclaration:
                case SyntaxKind.RecordDeclaration:
                    return ((TypeDeclarationSyntax)declaration).WithMembers(default);

                default:
                    return declaration;
            }
        }

        private static MemberDeclarationSyntax GetDeclarationSyntaxWithoutMembersWorker(
            INamedTypeSymbol namedType,
            CodeGenerationDestination destination,
            CSharpCodeGenerationContextInfo info)
        {
            if (namedType.TypeKind == TypeKind.Enum)
            {
                return GenerateEnumDeclaration(namedType, destination, info);
            }
            else if (namedType.TypeKind == TypeKind.Delegate)
            {
                return GenerateDelegateDeclaration(namedType, destination, info);
            }

            TypeDeclarationSyntax typeDeclaration;
            if (namedType.IsRecord)
            {
                var isRecordClass = namedType.TypeKind is TypeKind.Class;
                var declarationKind = isRecordClass ? SyntaxKind.RecordDeclaration : SyntaxKind.RecordStructDeclaration;
                var classOrStructKeyword = SyntaxFactory.Token(isRecordClass ? default : SyntaxKind.StructKeyword);

                typeDeclaration = SyntaxFactory.RecordDeclaration(kind: declarationKind, attributeLists: default, modifiers: default,
                    SyntaxFactory.Token(SyntaxKind.RecordKeyword), classOrStructKeyword, namedType.Name.ToIdentifierToken(),
                    typeParameterList: null, parameterList: null, baseList: null, constraintClauses: default, openBraceToken: default, members: default, closeBraceToken: default,
                    SyntaxFactory.Token(SyntaxKind.SemicolonToken));
            }
            else
            {
                var kind = namedType.TypeKind == TypeKind.Struct ? SyntaxKind.StructDeclaration :
                           namedType.TypeKind == TypeKind.Interface ? SyntaxKind.InterfaceDeclaration : SyntaxKind.ClassDeclaration;

                typeDeclaration = SyntaxFactory.TypeDeclaration(kind, namedType.Name.ToIdentifierToken());
            }

            var result = typeDeclaration
                .WithAttributeLists(GenerateAttributeDeclarations(namedType, info))
                .WithModifiers(GenerateModifiers(namedType, destination, info))
                .WithTypeParameterList(GenerateTypeParameterList(namedType, info))
                .WithBaseList(GenerateBaseList(namedType))
                .WithConstraintClauses(GenerateConstraintClauses(namedType));

            return result;
        }

        private static DelegateDeclarationSyntax GenerateDelegateDeclaration(
            INamedTypeSymbol namedType,
            CodeGenerationDestination destination,
            CSharpCodeGenerationContextInfo info)
        {
            var invokeMethod = namedType.DelegateInvokeMethod;
            Contract.ThrowIfNull(invokeMethod);

            return SyntaxFactory.DelegateDeclaration(
                GenerateAttributeDeclarations(namedType, info),
                GenerateModifiers(namedType, destination, info),
                invokeMethod.ReturnType.GenerateTypeSyntax(),
                namedType.Name.ToIdentifierToken(),
                TypeParameterGenerator.GenerateTypeParameterList(namedType.TypeParameters, info),
                ParameterGenerator.GenerateParameterList(invokeMethod.Parameters, isExplicit: false, info: info),
                namedType.TypeParameters.GenerateConstraintClauses());
        }

        private static EnumDeclarationSyntax GenerateEnumDeclaration(
            INamedTypeSymbol namedType,
            CodeGenerationDestination destination,
            CSharpCodeGenerationContextInfo info)
        {
            var baseList = namedType.EnumUnderlyingType != null && namedType.EnumUnderlyingType.SpecialType != SpecialType.System_Int32
                ? SyntaxFactory.BaseList(SyntaxFactory.SingletonSeparatedList<BaseTypeSyntax>(SyntaxFactory.SimpleBaseType(namedType.EnumUnderlyingType.GenerateTypeSyntax())))
                : null;

            return SyntaxFactory.EnumDeclaration(
                GenerateAttributeDeclarations(namedType, info),
                GenerateModifiers(namedType, destination, info),
                namedType.Name.ToIdentifierToken(),
                baseList: baseList,
                members: default);
        }

        private static SyntaxList<AttributeListSyntax> GenerateAttributeDeclarations(
            INamedTypeSymbol namedType, CSharpCodeGenerationContextInfo info)
        {
            return AttributeGenerator.GenerateAttributeLists(namedType.GetAttributes(), info);
        }

        private static SyntaxTokenList GenerateModifiers(
            INamedTypeSymbol namedType,
            CodeGenerationDestination destination,
            CSharpCodeGenerationContextInfo info)
        {
            var tokens = ArrayBuilder<SyntaxToken>.GetInstance();

            if (!namedType.IsFileLocal)
            {
                var defaultAccessibility = destination is CodeGenerationDestination.CompilationUnit or CodeGenerationDestination.Namespace
                    ? Accessibility.Internal
                    : Accessibility.Private;
                CSharpCodeGenerationHelpers.AddAccessibilityModifiers(namedType.DeclaredAccessibility, tokens, info, defaultAccessibility);
            }
            else
            {
                tokens.Add(SyntaxFactory.Token(SyntaxKind.FileKeyword));
            }

            if (namedType.IsStatic)
            {
                tokens.Add(SyntaxFactory.Token(SyntaxKind.StaticKeyword));
            }
            else
            {
                if (namedType.TypeKind == TypeKind.Class)
                {
                    if (namedType.IsAbstract)
                    {
                        tokens.Add(SyntaxFactory.Token(SyntaxKind.AbstractKeyword));
                    }

                    if (namedType.IsSealed)
                    {
                        tokens.Add(SyntaxFactory.Token(SyntaxKind.SealedKeyword));
                    }
                }
            }

            if (namedType.IsReadOnly)
            {
                tokens.Add(SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword));
            }

            if (namedType.IsRefLikeType)
            {
                tokens.Add(SyntaxFactory.Token(SyntaxKind.RefKeyword));
            }

            return tokens.ToSyntaxTokenListAndFree();
        }

        private static TypeParameterListSyntax? GenerateTypeParameterList(
            INamedTypeSymbol namedType, CSharpCodeGenerationContextInfo info)
        {
            return TypeParameterGenerator.GenerateTypeParameterList(namedType.TypeParameters, info);
        }

        private static BaseListSyntax? GenerateBaseList(INamedTypeSymbol namedType)
        {
            var types = new List<BaseTypeSyntax>();
            if (namedType.TypeKind == TypeKind.Class && namedType.BaseType != null && namedType.BaseType.SpecialType != Microsoft.CodeAnalysis.SpecialType.System_Object)
                types.Add(SyntaxFactory.SimpleBaseType(namedType.BaseType.GenerateTypeSyntax()));

            foreach (var type in namedType.Interfaces)
                types.Add(SyntaxFactory.SimpleBaseType(type.GenerateTypeSyntax()));

            if (types.Count == 0)
                return null;

            return SyntaxFactory.BaseList(SyntaxFactory.SeparatedList(types));
        }

        private static SyntaxList<TypeParameterConstraintClauseSyntax> GenerateConstraintClauses(INamedTypeSymbol namedType)
            => namedType.TypeParameters.GenerateConstraintClauses();
    }
}
