// 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.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.GenerateDefaultConstructors;

internal abstract partial class AbstractGenerateDefaultConstructorsService<TService>
{
    private sealed class State
    {
        public INamedTypeSymbol? ClassType { get; private set; }

        public ImmutableArray<IMethodSymbol> UnimplementedConstructors { get; private set; }

        private State()
        {
        }

        public static State? Generate(
            TService service,
            SemanticDocument document,
            TextSpan textSpan,
            bool forRefactoring,
            CancellationToken cancellationToken)
        {
            var state = new State();
            if (!state.TryInitialize(service, document, textSpan, forRefactoring, cancellationToken))
            {
                return null;
            }

            return state;
        }

        private bool TryInitialize(
            TService service,
            SemanticDocument semanticDocument,
            TextSpan textSpan,
            bool forRefactoring,
            CancellationToken cancellationToken)
        {
            if (!service.TryInitializeState(semanticDocument, textSpan, cancellationToken, out var classType))
                return false;

            ClassType = classType;

            var baseType = ClassType.BaseType;
            if (ClassType.IsStatic ||
                baseType == null ||
                baseType.TypeKind == TypeKind.Error)
            {
                return false;
            }

            // if this is for the refactoring, then don't offer this if the compiler is reporting an
            // error here.  We'll let the code fix take care of that.
            //
            // Similarly if this is for the codefix only offer if we do see that there's an error.
            var syntaxFacts = semanticDocument.Document.GetRequiredLanguageService<ISyntaxFactsService>();
            var headerFacts = semanticDocument.Document.GetRequiredLanguageService<IHeaderFactsService>();
            if (headerFacts.IsOnTypeHeader(semanticDocument.Root, textSpan.Start, fullHeader: true, out _))
            {
                var fixesError = FixesError(classType, baseType);
                if (forRefactoring == fixesError)
                    return false;
            }

            var semanticFacts = semanticDocument.Document.GetLanguageService<ISemanticFactsService>();
            var classConstructors = ClassType.InstanceConstructors;

            var destinationProvider = semanticDocument.Project.Solution.Services.GetLanguageServices(ClassType.Language);
            var isCaseSensitive = syntaxFacts.IsCaseSensitive;

            UnimplementedConstructors = baseType
                .InstanceConstructors
                .WhereAsArray(c => c.IsAccessibleWithin(ClassType) && IsMissing(c, classConstructors, isCaseSensitive));

            return UnimplementedConstructors.Length > 0;
        }

        private static bool FixesError(INamedTypeSymbol classType, INamedTypeSymbol baseType)
        {
            // See if the user didn't supply a constructor, and thus the compiler automatically generated
            // one for them.   If so, also see if there's an accessible no-arg contructor in the base.
            // If not, then the compiler will error and we want the code-fix to take over solving this problem.
            if (classType.Constructors.Any(static c => c.Parameters.Length == 0 && c.IsImplicitlyDeclared))
            {
                var baseNoArgConstructor = baseType.Constructors.FirstOrDefault(c => c.Parameters.Length == 0);
                if (baseNoArgConstructor == null ||
                    !baseNoArgConstructor.IsAccessibleWithin(classType))
                {
                    // this code is in error, but we're the refactoring codepath.  Offer nothing
                    // and let the code fix provider handle it instead.
                    return true;
                }

                // If this is a struct that has initializers, but is missing a parameterless constructor then we are fixing
                // an error (CS8983) but since this is the only scenario where we support structs we don't need to actually
                // check for anything else.
                if (classType.TypeKind == TypeKind.Struct)
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsMissing(
            IMethodSymbol constructor,
            ImmutableArray<IMethodSymbol> classConstructors,
            bool isCaseSensitive)
        {
            var matchingConstructor = classConstructors.FirstOrDefault(
                c => SignatureComparer.Instance.HaveSameSignature(
                    constructor.Parameters, c.Parameters, compareParameterName: true, isCaseSensitive: isCaseSensitive));

            if (matchingConstructor == null)
            {
                return true;
            }

            // We have a matching constructor in this type.  But we'll still offer to create the
            // constructor if the constructor that we have is implicit. 
            return matchingConstructor.IsImplicitlyDeclared;
        }
    }
}
