﻿// 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.GenerateMember.GenerateConstructor
{
    internal abstract partial class AbstractGenerateConstructorService<TService, TExpressionSyntax>
    {
        protected internal class State
        {
            private readonly TService _service;
            private readonly SemanticDocument _document;
            private readonly CodeAndImportGenerationOptionsProvider _fallbackOptions;

            private readonly NamingRule _fieldNamingRule;
            private readonly NamingRule _propertyNamingRule;
            private readonly NamingRule _parameterNamingRule;

            private ImmutableArray<Argument> _arguments;

            // The type we're creating a constructor for.  Will be a class or struct type.
            public INamedTypeSymbol? TypeToGenerateIn { get; private set; }

            private ImmutableArray<RefKind> _parameterRefKinds;
            public ImmutableArray<ITypeSymbol> ParameterTypes;

            public SyntaxToken Token { get; private set; }
            public bool IsConstructorInitializerGeneration { get; private set; }

            private IMethodSymbol? _delegatedConstructor;

            private ImmutableArray<IParameterSymbol> _parameters;
            private ImmutableDictionary<string, ISymbol>? _parameterToExistingMemberMap;

            public ImmutableDictionary<string, string> ParameterToNewFieldMap { get; private set; }
            public ImmutableDictionary<string, string> ParameterToNewPropertyMap { get; private set; }
            public bool IsContainedInUnsafeType { get; private set; }

            private State(TService service, SemanticDocument document, NamingRule fieldNamingRule, NamingRule propertyNamingRule, NamingRule parameterNamingRule, CodeAndImportGenerationOptionsProvider fallbackOptions)
            {
                _service = service;
                _document = document;
                _fieldNamingRule = fieldNamingRule;
                _propertyNamingRule = propertyNamingRule;
                _parameterNamingRule = parameterNamingRule;

                ParameterToNewFieldMap = ImmutableDictionary<string, string>.Empty;
                ParameterToNewPropertyMap = ImmutableDictionary<string, string>.Empty;
                _fallbackOptions = fallbackOptions;
            }

            public static async Task<State?> GenerateAsync(
                TService service,
                SemanticDocument document,
                SyntaxNode node,
                CodeAndImportGenerationOptionsProvider fallbackOptions,
                CancellationToken cancellationToken)
            {
                var fieldNamingRule = await document.Document.GetApplicableNamingRuleAsync(SymbolKind.Field, Accessibility.Private, fallbackOptions, cancellationToken).ConfigureAwait(false);
                var propertyNamingRule = await document.Document.GetApplicableNamingRuleAsync(SymbolKind.Property, Accessibility.Public, fallbackOptions, cancellationToken).ConfigureAwait(false);
                var parameterNamingRule = await document.Document.GetApplicableNamingRuleAsync(SymbolKind.Parameter, Accessibility.NotApplicable, fallbackOptions, cancellationToken).ConfigureAwait(false);

                var state = new State(service, document, fieldNamingRule, propertyNamingRule, parameterNamingRule, fallbackOptions);
                if (!await state.TryInitializeAsync(node, cancellationToken).ConfigureAwait(false))
                {
                    return null;
                }

                return state;
            }

            private async Task<bool> TryInitializeAsync(
                SyntaxNode node, CancellationToken cancellationToken)
            {
                if (_service.IsConstructorInitializerGeneration(_document, node, cancellationToken))
                {
                    if (!await TryInitializeConstructorInitializerGenerationAsync(node, cancellationToken).ConfigureAwait(false))
                        return false;
                }
                else if (_service.IsSimpleNameGeneration(_document, node, cancellationToken))
                {
                    if (!await TryInitializeSimpleNameGenerationAsync(node, cancellationToken).ConfigureAwait(false))
                        return false;
                }
                else if (_service.IsImplicitObjectCreation(_document, node, cancellationToken))
                {
                    if (!await TryInitializeImplicitObjectCreationAsync(node, cancellationToken).ConfigureAwait(false))
                        return false;
                }
                else
                {
                    return false;
                }

                Contract.ThrowIfNull(TypeToGenerateIn);
                if (!CodeGenerator.CanAdd(_document.Project.Solution, TypeToGenerateIn, cancellationToken))
                    return false;

                ParameterTypes = ParameterTypes.IsDefault ? GetParameterTypes(cancellationToken) : ParameterTypes;
                _parameterRefKinds = _arguments.SelectAsArray(a => a.RefKind);

                if (ClashesWithExistingConstructor())
                    return false;

                if (!TryInitializeDelegatedConstructor(cancellationToken))
                    InitializeNonDelegatedConstructor(cancellationToken);

                IsContainedInUnsafeType = _service.ContainingTypesOrSelfHasUnsafeKeyword(TypeToGenerateIn);

                return true;
            }

            private void InitializeNonDelegatedConstructor(CancellationToken cancellationToken)
            {
                var typeParametersNames = TypeToGenerateIn.GetAllTypeParameters().Select(t => t.Name).ToImmutableArray();
                var parameterNames = GetParameterNames(_arguments, typeParametersNames, cancellationToken);

                GetParameters(_arguments, ParameterTypes, parameterNames, cancellationToken);
            }

            private ImmutableArray<ParameterName> GetParameterNames(
                ImmutableArray<Argument> arguments, ImmutableArray<string> typeParametersNames, CancellationToken cancellationToken)
            {
                return _service.GenerateParameterNames(_document, arguments, typeParametersNames, _parameterNamingRule, cancellationToken);
            }

            private bool TryInitializeDelegatedConstructor(CancellationToken cancellationToken)
            {
                var parameters = ParameterTypes.Zip(_parameterRefKinds,
                    (t, r) => CodeGenerationSymbolFactory.CreateParameterSymbol(r, t, name: "")).ToImmutableArray();
                var expressions = _arguments.SelectAsArray(a => a.Expression);
                var delegatedConstructor = FindConstructorToDelegateTo(parameters, expressions, cancellationToken);
                if (delegatedConstructor == null)
                    return false;

                // Map the first N parameters to the other constructor in this type.  Then
                // try to map any further parameters to existing fields.  Finally, generate
                // new fields if no such parameters exist.

                // Find the names of the parameters that will follow the parameters we're
                // delegating.
                var argumentCount = delegatedConstructor.Parameters.Length;
                var remainingArguments = _arguments.Skip(argumentCount).ToImmutableArray();
                var remainingParameterNames = _service.GenerateParameterNames(
                    _document, remainingArguments,
                    delegatedConstructor.Parameters.Select(p => p.Name).ToList(),
                    _parameterNamingRule,
                    cancellationToken);

                // Can't generate the constructor if the parameter names we're copying over forcibly
                // conflict with any names we generated.
                if (delegatedConstructor.Parameters.Select(p => p.Name).Intersect(remainingParameterNames.Select(n => n.BestNameForParameter)).Any())
                    return false;

                var remainingParameterTypes = ParameterTypes.Skip(argumentCount).ToImmutableArray();

                _delegatedConstructor = delegatedConstructor;
                GetParameters(remainingArguments, remainingParameterTypes, remainingParameterNames, cancellationToken);
                return true;
            }

            private IMethodSymbol? FindConstructorToDelegateTo(
                ImmutableArray<IParameterSymbol> allParameters,
                ImmutableArray<TExpressionSyntax?> allExpressions,
                CancellationToken cancellationToken)
            {
                Contract.ThrowIfNull(TypeToGenerateIn);
                Contract.ThrowIfNull(TypeToGenerateIn.BaseType);

                for (var i = allParameters.Length; i > 0; i--)
                {
                    var parameters = allParameters.TakeAsArray(i);
                    var expressions = allExpressions.TakeAsArray(i);
                    var result = FindConstructorToDelegateTo(parameters, expressions, TypeToGenerateIn.InstanceConstructors, cancellationToken) ??
                                 FindConstructorToDelegateTo(parameters, expressions, TypeToGenerateIn.BaseType.InstanceConstructors, cancellationToken);
                    if (result != null)
                        return result;
                }

                return null;
            }

            private IMethodSymbol? FindConstructorToDelegateTo(
                ImmutableArray<IParameterSymbol> parameters,
                ImmutableArray<TExpressionSyntax?> expressions,
                ImmutableArray<IMethodSymbol> constructors,
                CancellationToken cancellationToken)
            {
                Contract.ThrowIfNull(TypeToGenerateIn);

                foreach (var constructor in constructors)
                {
                    // Don't bother delegating to an implicit constructor. We don't want to add `: base()` as that's just
                    // redundant for subclasses and `: this()` won't even work as we won't have an implicit constructor once
                    // we add this new constructor.
                    if (constructor.IsImplicitlyDeclared)
                        continue;

                    // Don't delegate to another constructor in this type if it's got the same parameter types as the
                    // one we're generating. This can happen if we're generating the new constructor because parameter
                    // names don't match (when a user explicitly provides named parameters).
                    if (TypeToGenerateIn.Equals(constructor.ContainingType) &&
                        constructor.Parameters.Select(p => p.Type).SequenceEqual(ParameterTypes))
                    {
                        continue;
                    }

                    if (GenerateConstructorHelpers.CanDelegateTo(_document, parameters, expressions, constructor) &&
                        !_service.WillCauseConstructorCycle(this, _document, constructor, cancellationToken))
                    {
                        return constructor;
                    }
                }

                return null;
            }

            private bool ClashesWithExistingConstructor()
            {
                Contract.ThrowIfNull(TypeToGenerateIn);

                var syntaxFacts = _document.Project.Solution.Services.GetRequiredLanguageService<ISyntaxFactsService>(TypeToGenerateIn.Language);
                return TypeToGenerateIn.InstanceConstructors.Any(static (c, arg) => arg.self.Matches(c, arg.syntaxFacts), (self: this, syntaxFacts));
            }

            private bool Matches(IMethodSymbol ctor, ISyntaxFactsService service)
            {
                if (ctor.Parameters.Length != ParameterTypes.Length)
                    return false;

                for (var i = 0; i < ParameterTypes.Length; i++)
                {
                    var ctorParameter = ctor.Parameters[i];
                    var result = SymbolEquivalenceComparer.Instance.Equals(ctorParameter.Type, ParameterTypes[i]) &&
                        ctorParameter.RefKind == _parameterRefKinds[i];

                    var parameterName = GetParameterName(i);
                    if (!string.IsNullOrEmpty(parameterName))
                    {
                        result &= service.IsCaseSensitive
                            ? ctorParameter.Name == parameterName
                            : string.Equals(ctorParameter.Name, parameterName, StringComparison.OrdinalIgnoreCase);
                    }

                    if (result == false)
                        return false;
                }

                return true;
            }

            private string GetParameterName(int index)
                => _arguments.IsDefault || index >= _arguments.Length ? string.Empty : _arguments[index].Name;

            internal ImmutableArray<ITypeSymbol> GetParameterTypes(CancellationToken cancellationToken)
            {
                var allTypeParameters = TypeToGenerateIn.GetAllTypeParameters();
                var semanticModel = _document.SemanticModel;
                var allTypes = _arguments.Select(a => _service.GetArgumentType(_document.SemanticModel, a, cancellationToken));

                return allTypes.Select(t => FixType(t, semanticModel, allTypeParameters)).ToImmutableArray();
            }

            private static ITypeSymbol FixType(ITypeSymbol typeSymbol, SemanticModel semanticModel, IEnumerable<ITypeParameterSymbol> allTypeParameters)
            {
                var compilation = semanticModel.Compilation;
                return typeSymbol.RemoveAnonymousTypes(compilation)
                    .RemoveUnavailableTypeParameters(compilation, allTypeParameters)
                    .RemoveUnnamedErrorTypes(compilation);
            }

            private async Task<bool> TryInitializeConstructorInitializerGenerationAsync(
                SyntaxNode constructorInitializer, CancellationToken cancellationToken)
            {
                if (_service.TryInitializeConstructorInitializerGeneration(
                        _document, constructorInitializer, cancellationToken,
                        out var token, out var arguments, out var typeToGenerateIn))
                {
                    Token = token;
                    _arguments = arguments;
                    IsConstructorInitializerGeneration = true;

                    var semanticInfo = _document.SemanticModel.GetSymbolInfo(constructorInitializer, cancellationToken);
                    if (semanticInfo.Symbol == null)
                        return await TryDetermineTypeToGenerateInAsync(typeToGenerateIn, cancellationToken).ConfigureAwait(false);
                }

                return false;
            }

            private async Task<bool> TryInitializeImplicitObjectCreationAsync(SyntaxNode implicitObjectCreation, CancellationToken cancellationToken)
            {
                if (_service.TryInitializeImplicitObjectCreation(
                        _document, implicitObjectCreation, cancellationToken,
                        out var token, out var arguments, out var typeToGenerateIn))
                {
                    Token = token;
                    _arguments = arguments;

                    var semanticInfo = _document.SemanticModel.GetSymbolInfo(implicitObjectCreation, cancellationToken);
                    if (semanticInfo.Symbol == null)
                        return await TryDetermineTypeToGenerateInAsync(typeToGenerateIn, cancellationToken).ConfigureAwait(false);
                }

                return false;
            }

            private async Task<bool> TryInitializeSimpleNameGenerationAsync(
                SyntaxNode simpleName,
                CancellationToken cancellationToken)
            {
                if (_service.TryInitializeSimpleNameGenerationState(
                        _document, simpleName, cancellationToken,
                        out var token, out var arguments, out var typeToGenerateIn))
                {
                    Token = token;
                    _arguments = arguments;
                }
                else if (_service.TryInitializeSimpleAttributeNameGenerationState(
                    _document, simpleName, cancellationToken, out token, out arguments, out typeToGenerateIn))
                {
                    Token = token;
                    _arguments = arguments;
                    //// Attribute parameters are restricted to be constant values (simple types or string, etc).
                    if (GetParameterTypes(cancellationToken).Any(static t => !IsValidAttributeParameterType(t)))
                        return false;
                }
                else
                {
                    return false;
                }

                cancellationToken.ThrowIfCancellationRequested();

                return await TryDetermineTypeToGenerateInAsync(typeToGenerateIn, cancellationToken).ConfigureAwait(false);
            }

            private static bool IsValidAttributeParameterType(ITypeSymbol type)
            {
                if (type.Kind == SymbolKind.ArrayType)
                {
                    var arrayType = (IArrayTypeSymbol)type;
                    if (arrayType.Rank != 1)
                    {
                        return false;
                    }

                    type = arrayType.ElementType;
                }

                if (type.IsEnumType())
                {
                    return true;
                }

                switch (type.SpecialType)
                {
                    case SpecialType.System_Boolean:
                    case SpecialType.System_Byte:
                    case SpecialType.System_Char:
                    case SpecialType.System_Int16:
                    case SpecialType.System_Int32:
                    case SpecialType.System_Int64:
                    case SpecialType.System_Double:
                    case SpecialType.System_Single:
                    case SpecialType.System_String:
                        return true;

                    default:
                        return false;
                }
            }

            private async Task<bool> TryDetermineTypeToGenerateInAsync(
                INamedTypeSymbol original, CancellationToken cancellationToken)
            {
                var definition = await SymbolFinder.FindSourceDefinitionAsync(original, _document.Project.Solution, cancellationToken).ConfigureAwait(false);
                TypeToGenerateIn = definition as INamedTypeSymbol;

                return TypeToGenerateIn?.TypeKind is (TypeKind?)TypeKind.Class or (TypeKind?)TypeKind.Struct;
            }

            private void GetParameters(
                ImmutableArray<Argument> arguments,
                ImmutableArray<ITypeSymbol> parameterTypes,
                ImmutableArray<ParameterName> parameterNames,
                CancellationToken cancellationToken)
            {
                var parameterToExistingMemberMap = ImmutableDictionary.CreateBuilder<string, ISymbol>();
                var parameterToNewFieldMap = ImmutableDictionary.CreateBuilder<string, string>();
                var parameterToNewPropertyMap = ImmutableDictionary.CreateBuilder<string, string>();

                using var _ = ArrayBuilder<IParameterSymbol>.GetInstance(out var parameters);

                for (var i = 0; i < parameterNames.Length; i++)
                {
                    var parameterName = parameterNames[i];
                    var parameterType = parameterTypes[i];
                    var argument = arguments[i];

                    // See if there's a matching field or property we can use, or create a new member otherwise.
                    FindExistingOrCreateNewMember(
                        ref parameterName, parameterType, argument,
                        parameterToExistingMemberMap, parameterToNewFieldMap, parameterToNewPropertyMap,
                        cancellationToken);

                    parameters.Add(CodeGenerationSymbolFactory.CreateParameterSymbol(
                        attributes: default,
                        refKind: argument.RefKind,
                        isParams: false,
                        type: parameterType,
                        name: parameterName.BestNameForParameter));
                }

                _parameters = parameters.ToImmutable();
                _parameterToExistingMemberMap = parameterToExistingMemberMap.ToImmutable();
                ParameterToNewFieldMap = parameterToNewFieldMap.ToImmutable();
                ParameterToNewPropertyMap = parameterToNewPropertyMap.ToImmutable();
            }

            private void FindExistingOrCreateNewMember(
                ref ParameterName parameterName,
                ITypeSymbol parameterType,
                Argument argument,
                ImmutableDictionary<string, ISymbol>.Builder parameterToExistingMemberMap,
                ImmutableDictionary<string, string>.Builder parameterToNewFieldMap,
                ImmutableDictionary<string, string>.Builder parameterToNewPropertyMap,
                CancellationToken cancellationToken)
            {
                var expectedFieldName = _fieldNamingRule.NamingStyle.MakeCompliant(parameterName.NameBasedOnArgument).First();
                var expectedPropertyName = _propertyNamingRule.NamingStyle.MakeCompliant(parameterName.NameBasedOnArgument).First();
                var isFixed = argument.IsNamed;

                // For non-out parameters, see if there's already a field there with the same name.
                // If so, and it has a compatible type, then we can just assign to that field.
                // Otherwise, we'll need to choose a different name for this member so that it
                // doesn't conflict with something already in the type. First check the current type
                // for a matching field.  If so, defer to it.

                var unavailableMemberNames = GetUnavailableMemberNames().ToImmutableArray();

                var members = from t in TypeToGenerateIn.GetBaseTypesAndThis()
                              let ignoreAccessibility = t.Equals(TypeToGenerateIn)
                              from m in t.GetMembers()
                              where m.Name.Equals(expectedFieldName, StringComparison.OrdinalIgnoreCase)
                              where ignoreAccessibility || IsSymbolAccessible(m, _document)
                              select m;

                var membersArray = members.ToImmutableArray();
                var symbol = membersArray.FirstOrDefault(m => m.Name.Equals(expectedFieldName, StringComparison.Ordinal)) ?? membersArray.FirstOrDefault();
                if (symbol != null)
                {
                    if (IsViableFieldOrProperty(parameterType, symbol))
                    {
                        // Ok!  We can just the existing field.  
                        parameterToExistingMemberMap[parameterName.BestNameForParameter] = symbol;
                    }
                    else
                    {
                        // Uh-oh.  Now we have a problem.  We can't assign this parameter to
                        // this field.  So we need to create a new field.  Find a name not in
                        // use so we can assign to that.  
                        var baseName = _service.GenerateNameForArgument(_document.SemanticModel, argument, cancellationToken);

                        var baseFieldWithNamingStyle = _fieldNamingRule.NamingStyle.MakeCompliant(baseName).First();
                        var basePropertyWithNamingStyle = _propertyNamingRule.NamingStyle.MakeCompliant(baseName).First();

                        var newFieldName = NameGenerator.EnsureUniqueness(baseFieldWithNamingStyle, unavailableMemberNames.Concat(parameterToNewFieldMap.Values));
                        var newPropertyName = NameGenerator.EnsureUniqueness(basePropertyWithNamingStyle, unavailableMemberNames.Concat(parameterToNewPropertyMap.Values));

                        if (isFixed)
                        {
                            // Can't change the parameter name, so map the existing parameter
                            // name to the new field name.
                            parameterToNewFieldMap[parameterName.NameBasedOnArgument] = newFieldName;
                            parameterToNewPropertyMap[parameterName.NameBasedOnArgument] = newPropertyName;
                        }
                        else
                        {
                            // Can change the parameter name, so do so.  
                            // But first remove any prefix added due to field naming styles
                            var fieldNameMinusPrefix = newFieldName[_fieldNamingRule.NamingStyle.Prefix.Length..];
                            var newParameterName = new ParameterName(fieldNameMinusPrefix, isFixed: false, _parameterNamingRule);
                            parameterName = newParameterName;

                            parameterToNewFieldMap[newParameterName.BestNameForParameter] = newFieldName;
                            parameterToNewPropertyMap[newParameterName.BestNameForParameter] = newPropertyName;
                        }
                    }

                    return;
                }

                // If no matching field was found, use the fieldNamingRule to create suitable name
                var bestNameForParameter = parameterName.BestNameForParameter;
                var nameBasedOnArgument = parameterName.NameBasedOnArgument;
                parameterToNewFieldMap[bestNameForParameter] = _fieldNamingRule.NamingStyle.MakeCompliant(nameBasedOnArgument).First();
                parameterToNewPropertyMap[bestNameForParameter] = _propertyNamingRule.NamingStyle.MakeCompliant(nameBasedOnArgument).First();
            }

            private IEnumerable<string> GetUnavailableMemberNames()
            {
                Contract.ThrowIfNull(TypeToGenerateIn);

                return TypeToGenerateIn.MemberNames.Concat(
                    from type in TypeToGenerateIn.GetBaseTypes()
                    from member in type.GetMembers()
                    select member.Name);
            }

            private bool IsViableFieldOrProperty(
                ITypeSymbol parameterType,
                ISymbol symbol)
            {
                if (parameterType.Language != symbol.Language)
                    return false;

                if (symbol != null && !symbol.IsStatic)
                {
                    if (symbol is IFieldSymbol field)
                    {
                        return
                            !field.IsConst &&
                            _service.IsConversionImplicit(_document.SemanticModel.Compilation, parameterType, field.Type);
                    }
                    else if (symbol is IPropertySymbol property)
                    {
                        return
                            property.Parameters.Length == 0 &&
                            property.IsWritableInConstructor() &&
                            _service.IsConversionImplicit(_document.SemanticModel.Compilation, parameterType, property.Type);
                    }
                }

                return false;
            }

            public async Task<Document> GetChangedDocumentAsync(
                Document document, bool withFields, bool withProperties, CancellationToken cancellationToken)
            {
                // See if there's an accessible base constructor that would accept these
                // types, then just call into that instead of generating fields.
                //
                // then, see if there are any constructors that would take the first 'n' arguments
                // we've provided.  If so, delegate to those, and then create a field for any
                // remaining arguments.  Try to match from largest to smallest.
                //
                // Otherwise, just generate a normal constructor that assigns any provided
                // parameters into fields.
                return await GenerateThisOrBaseDelegatingConstructorAsync(document, withFields, withProperties, cancellationToken).ConfigureAwait(false) ??
                       await GenerateMemberDelegatingConstructorAsync(document, withFields, withProperties, cancellationToken).ConfigureAwait(false);
            }

            private async Task<Document?> GenerateThisOrBaseDelegatingConstructorAsync(
                Document document, bool withFields, bool withProperties, CancellationToken cancellationToken)
            {
                if (_delegatedConstructor == null)
                    return null;

                Contract.ThrowIfNull(TypeToGenerateIn);

                var provider = document.Project.Solution.Services.GetLanguageServices(TypeToGenerateIn.Language);
                var (members, assignments) = await GenerateMembersAndAssignmentsAsync(document, withFields, withProperties, cancellationToken).ConfigureAwait(false);
                var isThis = _delegatedConstructor.ContainingType.OriginalDefinition.Equals(TypeToGenerateIn.OriginalDefinition);
                var delegatingArguments = provider.GetService<SyntaxGenerator>().CreateArguments(_delegatedConstructor.Parameters);

                var newParameters = _delegatedConstructor.Parameters.Concat(_parameters);
                var generateUnsafe = !IsContainedInUnsafeType && newParameters.Any(static p => p.RequiresUnsafeModifier());

                var constructor = CodeGenerationSymbolFactory.CreateConstructorSymbol(
                    attributes: default,
                    accessibility: Accessibility.Public,
                    modifiers: new DeclarationModifiers(isUnsafe: generateUnsafe),
                    typeName: TypeToGenerateIn.Name,
                    parameters: newParameters,
                    statements: assignments,
                    baseConstructorArguments: isThis ? default : delegatingArguments,
                    thisConstructorArguments: isThis ? delegatingArguments : default);

                var context = new CodeGenerationSolutionContext(
                    document.Project.Solution,
                    new CodeGenerationContext(Token.GetLocation()),
                    _fallbackOptions);

                return await provider.GetRequiredService<ICodeGenerationService>().AddMembersAsync(
                    context,
                    TypeToGenerateIn,
                    members.Concat(constructor),
                    cancellationToken).ConfigureAwait(false);
            }

            private async Task<(ImmutableArray<ISymbol>, ImmutableArray<SyntaxNode>)> GenerateMembersAndAssignmentsAsync(
                Document document, bool withFields, bool withProperties, CancellationToken cancellationToken)
            {
                Contract.ThrowIfNull(TypeToGenerateIn);

                var provider = document.Project.Solution.Services.GetLanguageServices(TypeToGenerateIn.Language);

                var members = withFields ? SyntaxGeneratorExtensions.CreateFieldsForParameters(_parameters, ParameterToNewFieldMap, IsContainedInUnsafeType) :
                              withProperties ? SyntaxGeneratorExtensions.CreatePropertiesForParameters(_parameters, ParameterToNewPropertyMap, IsContainedInUnsafeType) :
                              ImmutableArray<ISymbol>.Empty;

                var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                var assignments = !withFields && !withProperties
                    ? ImmutableArray<SyntaxNode>.Empty
                    : provider.GetRequiredService<SyntaxGenerator>().CreateAssignmentStatements(
                        semanticModel, _parameters,
                        _parameterToExistingMemberMap,
                        withFields ? ParameterToNewFieldMap : ParameterToNewPropertyMap,
                        addNullChecks: false, preferThrowExpression: false);

                return (members, assignments);
            }

            private async Task<Document> GenerateMemberDelegatingConstructorAsync(
                Document document, bool withFields, bool withProperties, CancellationToken cancellationToken)
            {
                Contract.ThrowIfNull(TypeToGenerateIn);

                var provider = document.Project.Solution.Services.GetLanguageServices(TypeToGenerateIn.Language);
                var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);

                var newMemberMap =
                    withFields ? ParameterToNewFieldMap :
                    withProperties ? ParameterToNewPropertyMap :
                    ImmutableDictionary<string, string>.Empty;

                return await provider.GetRequiredService<ICodeGenerationService>().AddMembersAsync(
                    new CodeGenerationSolutionContext(
                        document.Project.Solution,
                        new CodeGenerationContext(Token.GetLocation()),
                        _fallbackOptions),
                    TypeToGenerateIn,
                    provider.GetRequiredService<SyntaxGenerator>().CreateMemberDelegatingConstructor(
                        semanticModel,
                        TypeToGenerateIn.Name,
                        TypeToGenerateIn,
                        _parameters,
                        TypeToGenerateIn.IsAbstractClass() ? Accessibility.Protected : Accessibility.Public,
                        _parameterToExistingMemberMap,
                        newMemberMap,
                        addNullChecks: false,
                        preferThrowExpression: false,
                        generateProperties: withProperties,
                        IsContainedInUnsafeType),
                    cancellationToken).ConfigureAwait(false);
            }
        }
    }
}
