using System;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.Kinds;

namespace Semmle.Extraction.CSharp.Entities.Expressions
{
    internal class Invocation : Expression<InvocationExpressionSyntax>
    {
        private Invocation(ExpressionNodeInfo info)
            : base(info.SetKind(GetKind(info)))
        {
            this.info = info;
        }

        private readonly ExpressionNodeInfo info;

        public static Expression Create(ExpressionNodeInfo info) => new Invocation(info).TryPopulate();

        private bool IsEventDelegateCall() => Kind == ExprKind.DELEGATE_INVOCATION && Context.GetModel(Syntax.Expression).GetSymbolInfo(Syntax.Expression).Symbol?.Kind == SymbolKind.Event;

        private bool IsExplicitDelegateInvokeCall() => Kind == ExprKind.DELEGATE_INVOCATION && Context.GetModel(Syntax.Expression).GetSymbolInfo(Syntax.Expression).Symbol is IMethodSymbol m && m.MethodKind == MethodKind.DelegateInvoke;

        private bool IsOperatorCall() => Kind == ExprKind.OPERATOR_INVOCATION;

        private bool IsValidMemberAccessKind()
        {
            return Kind == ExprKind.METHOD_INVOCATION ||
                IsEventDelegateCall() ||
                IsExplicitDelegateInvokeCall() ||
                IsOperatorCall();
        }

        protected override void PopulateExpression(TextWriter trapFile)
        {
            if (IsNameof(Syntax))
            {
                PopulateArguments(trapFile, Syntax.ArgumentList, 0);
                return;
            }

            var child = -1;
            string? memberName = null;
            var target = GetTargetSymbol(Context, Syntax);
            switch (Syntax.Expression)
            {
                case MemberAccessExpressionSyntax memberAccess when IsValidMemberAccessKind():
                    memberName = memberAccess.Name.Identifier.Text;
                    if (Syntax.Expression.Kind() == SyntaxKind.SimpleMemberAccessExpression)
                        // Qualified method call; `x.M()`
                        Create(Context, memberAccess.Expression, this, child++);
                    else
                        // Pointer member access; `x->M()`
                        Create(Context, Syntax.Expression, this, child++);
                    break;
                case MemberBindingExpressionSyntax memberBinding:
                    // Conditionally qualified method call; `x?.M()`
                    memberName = memberBinding.Name.Identifier.Text;
                    Create(Context, FindConditionalQualifier(memberBinding), this, child++);
                    MakeConditional(trapFile);
                    break;
                case SimpleNameSyntax simpleName when Kind == ExprKind.METHOD_INVOCATION:
                    // Unqualified method call; `M()`
                    memberName = simpleName.Identifier.Text;
                    if (target is not null && !target.IsStatic)
                    {
                        // Implicit `this` qualifier; add explicitly
                        if (Location.Symbol is not null &&
                            Context.GetModel(Syntax).GetEnclosingSymbol(Location.Symbol.SourceSpan.Start) is IMethodSymbol callingMethod)
                        {
                            This.CreateImplicit(Context, callingMethod.ContainingType, Location, this, child++);
                        }
                        else
                        {
                            Context.ModelError(Syntax, "Couldn't determine implicit this type");
                        }
                    }
                    else
                    {
                        // No implicit `this` qualifier
                        child++;
                    }
                    break;
                default:
                    // Delegate or function pointer call; `d()`
                    Create(Context, Syntax.Expression, this, child++);
                    break;
            }

            var isDynamicCall = IsDynamicCall(info);
            if (isDynamicCall)
            {
                if (memberName is not null)
                    trapFile.dynamic_member_name(this, memberName);
                else
                    Context.ModelError(Syntax, "Unable to get name for dynamic call.");
            }

            PopulateArguments(trapFile, Syntax.ArgumentList, child);

            if (target is null)
            {
                if (!isDynamicCall && !IsDelegateLikeCall(info))
                    Context.ModelError(Syntax, "Unable to resolve target for call. (Compilation error?)");
                return;
            }

            var targetKey = Method.Create(Context, target);
            trapFile.expr_call(this, targetKey);
        }

        private static bool IsDynamicCall(ExpressionNodeInfo info)
        {
            // Either the qualifier (Expression) is dynamic,
            // or one of the arguments is dynamic.
            var node = (InvocationExpressionSyntax)info.Node;
            return !IsDelegateLikeCall(info) &&
                (IsDynamic(info.Context, node.Expression) || node.ArgumentList.Arguments.Any(arg => IsDynamic(info.Context, arg.Expression)));
        }

        public SymbolInfo SymbolInfo => info.SymbolInfo;

        private static bool IsOperatorLikeCall(ExpressionNodeInfo info)
        {
            return info.SymbolInfo.Symbol is IMethodSymbol method &&
                method.TryGetExtensionMethod()?.MethodKind == MethodKind.UserDefinedOperator;
        }

        private static bool IsDelegateLikeCall(ExpressionNodeInfo info)
        {
            return IsDelegateLikeCall(info, symbol => IsFunctionPointer(symbol) || IsDelegateInvoke(symbol));
        }

        private static bool IsDelegateInvokeCall(ExpressionNodeInfo info)
        {
            return IsDelegateLikeCall(info, IsDelegateInvoke);
        }

        private static bool IsDelegateLikeCall(ExpressionNodeInfo info, Func<ISymbol?, bool> check)
        {
            var si = info.SymbolInfo;

            if (si.CandidateReason == CandidateReason.OverloadResolutionFailure &&
                si.CandidateSymbols.All(check))
            {
                return true;
            }

            // Delegate variable is a dynamic
            var node = (InvocationExpressionSyntax)info.Node;
            if (si.CandidateReason == CandidateReason.LateBound &&
                node.Expression is IdentifierNameSyntax &&
                IsDynamic(info.Context, node.Expression) &&
                si.Symbol is null)
            {
                return true;
            }

            return check(si.Symbol);
        }

        private static bool IsFunctionPointer(ISymbol? symbol)
        {
            return symbol is not null &&
                symbol.Kind == SymbolKind.FunctionPointerType;
        }

        private static bool IsDelegateInvoke(ISymbol? symbol)
        {
            return symbol is not null &&
                symbol.Kind == SymbolKind.Method &&
                ((IMethodSymbol)symbol).MethodKind == MethodKind.DelegateInvoke;
        }

        private static bool IsLocalFunctionInvocation(ExpressionNodeInfo info)
        {
            return info.SymbolInfo.Symbol is IMethodSymbol target &&
                target.MethodKind == MethodKind.LocalFunction;
        }

        private static ExprKind GetKind(ExpressionNodeInfo info)
        {
            if (IsNameof((InvocationExpressionSyntax)info.Node))
            {
                return ExprKind.NAMEOF;
            }
            if (IsDelegateLikeCall(info))
            {
                return IsDelegateInvokeCall(info)
                    ? ExprKind.DELEGATE_INVOCATION
                    : ExprKind.FUNCTION_POINTER_INVOCATION;
            }
            if (IsLocalFunctionInvocation(info))
            {
                return ExprKind.LOCAL_FUNCTION_INVOCATION;
            }
            if (IsOperatorLikeCall(info))
            {
                return ExprKind.OPERATOR_INVOCATION;
            }
            return ExprKind.METHOD_INVOCATION;
        }

        private static bool IsNameof(InvocationExpressionSyntax syntax)
        {
            // Odd that this is not a separate expression type.
            // Maybe it will be in the future.
            return syntax.Expression is IdentifierNameSyntax id && id.Identifier.Text == "nameof";
        }
    }
}
