using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Util;

namespace Semmle.Extraction.CSharp.Entities
{
    internal class Constructor : Method
    {
        private readonly List<SyntaxNode> declaringReferenceSyntax;

        private Constructor(Context cx, IMethodSymbol init)
            : base(cx, init)
        {
            declaringReferenceSyntax =
                Symbol.DeclaringSyntaxReferences
                    .Select(r => r.GetSyntax())
                    .ToList();
        }

        public override void Populate(TextWriter trapFile)
        {
            PopulateMethod(trapFile);
            PopulateModifiers(trapFile);
            ContainingType!.PopulateGenerics();

            trapFile.constructors(this, Symbol.ContainingType.Name, ContainingType, (Constructor)OriginalDefinition);

            if (Symbol.IsImplicitlyDeclared)
            {
                var lineCounts = new LineCounts() { Total = 2, Code = 1, Comment = 0 };
                trapFile.numlines(this, lineCounts);
            }
            ExtractCompilerGenerated(trapFile);

            if (Context.OnlyScaffold)
            {
                return;
            }

            if (MakeSynthetic)
            {
                // Create a synthetic empty body for primary and default constructors.
                Statements.SyntheticEmptyBlock.Create(Context, this, 0, Location);
            }

            if (Context.ExtractLocation(Symbol) && (!IsDefault || IsBestSourceLocation))
            {
                WriteLocationToTrap(trapFile.constructor_location, this, Location);
            }

        }

        protected override void ExtractInitializers(TextWriter trapFile)
        {
            // Do not extract initializers for constructed types.
            // Extract initializers for constructors with a body, primary constructors
            // and default constructors for classes and structs declared in source code.
            if (Block is null && ExpressionBody is null && !MakeSynthetic || Context.OnlyScaffold)
            {
                return;
            }

            if (OrdinaryConstructorSyntax?.Initializer is ConstructorInitializerSyntax initializer)
            {
                ITypeSymbol initializerType;
                var initializerInfo = Context.GetSymbolInfo(initializer);

                switch (initializer.Kind())
                {
                    case SyntaxKind.BaseConstructorInitializer:
                        initializerType = Symbol.ContainingType.BaseType!;
                        ExtractObjectInitCall(trapFile);
                        break;
                    case SyntaxKind.ThisConstructorInitializer:
                        initializerType = Symbol.ContainingType;
                        break;
                    default:
                        Context.ModelError(initializer, "Unknown initializer");
                        return;
                }

                ExtractSourceInitializer(trapFile, initializerType, (IMethodSymbol?)initializerInfo.Symbol, initializer.ArgumentList, initializer.ThisOrBaseKeyword.GetLocation());
            }
            else if (PrimaryBase is PrimaryConstructorBaseTypeSyntax primaryInitializer)
            {
                var primaryInfo = Context.GetSymbolInfo(primaryInitializer);
                var primarySymbol = primaryInfo.Symbol;

                ExtractObjectInitCall(trapFile);
                ExtractSourceInitializer(trapFile, primarySymbol?.ContainingType, (IMethodSymbol?)primarySymbol, primaryInitializer.ArgumentList, primaryInitializer.GetLocation());
            }
            else if (Symbol.MethodKind is MethodKind.Constructor)
            {
                ExtractObjectInitCall(trapFile);
                var baseType = Symbol.ContainingType.BaseType;
                if (baseType is null)
                {
                    if (Symbol.ContainingType.SpecialType != SpecialType.System_Object)
                    {
                        Context.ModelError(Symbol, "Unable to resolve base type in implicit constructor initializer");
                    }
                    return;
                }

                var baseConstructor = baseType.InstanceConstructors.FirstOrDefault(c => c.Arity is 0);

                if (baseConstructor is null)
                {
                    Context.ModelError(Symbol, "Unable to resolve implicit constructor initializer call");
                    return;
                }

                var baseConstructorTarget = Create(Context, baseConstructor);

                var info = new ExpressionInfo(Context,
                    AnnotatedTypeSymbol.CreateNotAnnotated(baseType),
                    Location,
                    Kinds.ExprKind.CONSTRUCTOR_INIT,
                    this,
                    -1,
                    isCompilerGenerated: true,
                    null);

                trapFile.expr_call(new Expression(info), baseConstructorTarget);
            }
        }

        private void ExtractObjectInitCall(TextWriter trapFile)
        {
            var target = ObjectInitMethod.Create(Context, ContainingType!);

            var type = Context.Compilation.GetSpecialType(SpecialType.System_Void);

            var info = new ExpressionInfo(Context,
                AnnotatedTypeSymbol.CreateNotAnnotated(type),
                Location,
                Kinds.ExprKind.METHOD_INVOCATION,
                this,
                -2,
                isCompilerGenerated: true,
                null);
            var obinitCall = new Expression(info);

            trapFile.expr_call(obinitCall, target);

            Expressions.This.CreateImplicit(Context, Symbol.ContainingType, Location, obinitCall, -1);
        }

        private void ExtractSourceInitializer(TextWriter trapFile, ITypeSymbol? type, IMethodSymbol? symbol, ArgumentListSyntax arguments, Microsoft.CodeAnalysis.Location location)
        {
            var initInfo = new ExpressionInfo(Context,
                AnnotatedTypeSymbol.CreateNotAnnotated(type),
                Context.CreateLocation(location),
                Kinds.ExprKind.CONSTRUCTOR_INIT,
                this,
                -1,
                false,
                null);

            var init = new Expression(initInfo);

            var target = Constructor.Create(Context, symbol);
            if (target is null)
            {
                Context.ModelError(Symbol, "Unable to resolve call");
                return;
            }

            trapFile.expr_call(init, target);

            init.PopulateArguments(trapFile, arguments, 0);
        }

        private ConstructorDeclarationSyntax? OrdinaryConstructorSyntax =>
            declaringReferenceSyntax
                .OfType<ConstructorDeclarationSyntax>()
                .FirstOrDefault();

        private TypeDeclarationSyntax? PrimaryConstructorSyntax =>
            declaringReferenceSyntax
                    .OfType<TypeDeclarationSyntax>()
                    .FirstOrDefault(t => t is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax);

        private PrimaryConstructorBaseTypeSyntax? PrimaryBase =>
            PrimaryConstructorSyntax?
                .BaseList?
                .Types
                .OfType<PrimaryConstructorBaseTypeSyntax>()
                .FirstOrDefault();

        private bool IsPrimary => PrimaryConstructorSyntax is not null;

        // This is a default constructor in a class or struct declared in source.
        private bool IsDefault =>
            Symbol.IsImplicitlyDeclared &&
            Symbol.ContainingType.FromSource() &&
            Symbol.ContainingType.TypeKind is TypeKind.Class or TypeKind.Struct &&
            Symbol.ContainingType.IsSourceDeclaration() &&
            !Symbol.ContainingType.IsAnonymousType;

        /// <summary>
        /// Returns true if we consider the reporting location of this constructor entity the best
        /// location of the constructor.
        /// For partial classes with default constructors, Roslyn consider each partial class declaration
        /// as the possible location for the implicit default constructor.
        /// </summary>
        private bool IsBestSourceLocation => ReportingLocation is not null && Context.IsLocationInContext(ReportingLocation);

        private bool MakeSynthetic => (IsPrimary || (IsDefault && IsBestSourceLocation)) && !Context.OnlyScaffold;

        [return: NotNullIfNotNull(nameof(constructor))]
        public static new Constructor? Create(Context cx, IMethodSymbol? constructor)
        {
            if (constructor is null)
                return null;

            switch (constructor.MethodKind)
            {
                case MethodKind.StaticConstructor:
                case MethodKind.Constructor:
                    return ConstructorFactory.Instance.CreateEntityFromSymbol(cx, constructor);
                default:
                    throw new InternalError(constructor, "Attempt to create a Constructor from a symbol that isn't a constructor");
            }
        }

        public override void WriteId(EscapingTextWriter trapFile)
        {
            if (!SymbolEqualityComparer.Default.Equals(Symbol, Symbol.OriginalDefinition))
            {
                trapFile.WriteSubId(ContainingType!);
                trapFile.Write(".");
                trapFile.WriteSubId(OriginalDefinition);
                trapFile.Write(";constructor");
                return;
            }

            if (Symbol.IsStatic)
                trapFile.Write("static");
            trapFile.WriteSubId(ContainingType!);
            AddParametersToId(Context, trapFile, Symbol);
            trapFile.Write(";constructor");
        }

        public override Microsoft.CodeAnalysis.Location? FullLocation => ReportingLocation;

        public override Microsoft.CodeAnalysis.Location? ReportingLocation
        {
            get
            {
                if (OrdinaryConstructorSyntax is not null)
                {
                    return OrdinaryConstructorSyntax.Identifier.GetLocation();
                }

                if (PrimaryConstructorSyntax is not null)
                {
                    return PrimaryConstructorSyntax.Identifier.GetLocation();
                }

                if (Symbol.IsImplicitlyDeclared)
                {
                    var best = Symbol.Locations.Where(l => l.IsInSource).BestOrDefault();
                    return best ?? ContainingType!.ReportingLocation;
                }

                return Symbol.ContainingType.Locations.FirstOrDefault();
            }
        }

        private class ConstructorFactory : CachedEntityFactory<IMethodSymbol, Constructor>
        {
            public static ConstructorFactory Instance { get; } = new ConstructorFactory();

            public override Constructor Create(Context cx, IMethodSymbol init) => new Constructor(cx, init);
        }
    }
}
