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

namespace Semmle.Extraction.CSharp.Entities
{
    public interface IExpressionParentEntity : IEntity
    {
        /// <summary>
        /// Whether this entity is the parent of a top-level expression.
        /// </summary>
        bool IsTopLevelParent { get; }
    }

    class Expression : FreshEntity, IExpressionParentEntity
    {
        private readonly IExpressionInfo Info;
        public readonly AnnotatedType Type;
        public readonly Extraction.Entities.Location Location;
        public readonly ExprKind Kind;

        internal Expression(IExpressionInfo info)
            : base(info.Context)
        {
            Info = info;
            Location = info.Location;
            Kind = info.Kind;
            Type = info.Type;
            if (Type.Type is null)
                Type = NullType.Create(cx);

            TryPopulate();
        }

        protected sealed override void Populate(TextWriter trapFile)
        {
            trapFile.expressions(this, Kind, Type.Type.TypeRef);
            if (Info.Parent.IsTopLevelParent)
                trapFile.expr_parent_top_level(this, Info.Child, Info.Parent);
            else
                trapFile.expr_parent(this, Info.Child, Info.Parent);
            trapFile.expr_location(this, Location);

            if (Info.IsCompilerGenerated)
                trapFile.expr_compiler_generated(this);

            if (Info.ExprValue is string value)
                trapFile.expr_value(this, value);

            Type.Type.PopulateGenerics();
        }

        public override Microsoft.CodeAnalysis.Location ReportingLocation => Location.symbol;

        bool IExpressionParentEntity.IsTopLevelParent => false;

        /// <summary>
        /// Gets a string represention of a constant value.
        /// </summary>
        /// <param name="obj">The value.</param>
        /// <returns>The string representation.</returns>
        public static string ValueAsString(object value)
        {
            return value == null ? "null" : value is bool ? ((bool)value ? "true" : "false") : value.ToString();
        }

        /// <summary>
        /// Creates an expression from a syntax node.
        /// Inserts type conversion as required.
        /// </summary>
        /// <param name="cx">The extraction context.</param>
        /// <param name="node">The node to extract.</param>
        /// <param name="parent">The parent entity.</param>
        /// <param name="child">The child index.</param>
        /// <param name="type">A type hint.</param>
        /// <returns>The new expression.</returns>
        public static Expression Create(Context cx, ExpressionSyntax node, IExpressionParentEntity parent, int child) =>
            CreateFromNode(new ExpressionNodeInfo(cx, node, parent, child));

        public static Expression CreateFromNode(ExpressionNodeInfo info) => Expressions.ImplicitCast.Create(info);

        /// <summary>
        /// Creates an expression from a syntax node.
        /// Inserts type conversion as required.
        /// Population is deferred to avoid overflowing the stack.
        /// </summary>
        /// <param name="cx">The extraction context.</param>
        /// <param name="node">The node to extract.</param>
        /// <param name="parent">The parent entity.</param>
        /// <param name="child">The child index.</param>
        /// <param name="type">A type hint.</param>
        public static void CreateDeferred(Context cx, ExpressionSyntax node, IExpressionParentEntity parent, int child)
        {
            if (ContainsPattern(node))
                // Expressions with patterns should be created right away, as they may introduce
                // local variables referenced in `LocalVariable::GetAlreadyCreated()`
                Create(cx, node, parent, child);
            else
                cx.PopulateLater(() => Create(cx, node, parent, child));
        }

        static bool ContainsPattern(SyntaxNode node) =>
            node is PatternSyntax || node is VariableDesignationSyntax || node.ChildNodes().Any(ContainsPattern);

        /// <summary>
        /// Adapt the operator kind depending on whether it's a dynamic call or a user-operator call.
        /// </summary>
        /// <param name="cx"></param>
        /// <param name="node"></param>
        /// <param name="originalKind"></param>
        /// <returns></returns>
        public static ExprKind UnaryOperatorKind(Context cx, ExprKind originalKind, ExpressionSyntax node) =>
            GetCallType(cx, node).AdjustKind(originalKind);

        /// <summary>
        /// If the expression calls an operator, add an expr_call()
        /// to show the target of the call. Also note the dynamic method
        /// name if available.
        /// </summary>
        /// <param name="cx">Context</param>
        /// <param name="node">The expression.</param>
        public void OperatorCall(TextWriter trapFile, ExpressionSyntax node)
        {
            var @operator = cx.GetSymbolInfo(node);
            if (@operator.Symbol is IMethodSymbol method)
            {
                var callType = GetCallType(cx, node);
                if (callType == CallType.Dynamic)
                {
                    UserOperator.OperatorSymbol(method.Name, out string operatorName);
                    trapFile.dynamic_member_name(this, operatorName);
                    return;
                }

                trapFile.expr_call(this, Method.Create(cx, method));
            }
        }

        public enum CallType
        {
            None,
            BuiltInOperator,
            Dynamic,
            UserOperator
        }

        /// <summary>
        /// Determine what type of method was called for this expression.
        /// </summary>
        /// <param name="cx">The context.</param>
        /// <param name="node">The expression</param>
        /// <returns>The call type.</returns>
        public static CallType GetCallType(Context cx, ExpressionSyntax node)
        {
            var @operator = cx.GetSymbolInfo(node);

            if (@operator.Symbol is IMethodSymbol method)
            {
                if (method.ContainingSymbol is ITypeSymbol containingSymbol && containingSymbol.TypeKind == Microsoft.CodeAnalysis.TypeKind.Dynamic)
                {
                    return CallType.Dynamic;
                }

                switch (method.MethodKind)
                {
                    case MethodKind.BuiltinOperator:
                        if (method.ContainingType != null && method.ContainingType.TypeKind == Microsoft.CodeAnalysis.TypeKind.Delegate)
                            return CallType.UserOperator;
                        return CallType.BuiltInOperator;
                    case MethodKind.Constructor:
                        // The index operator ^... generates a constructor call to System.Index.
                        // Instead, treat this as a regular operator.
                        return CallType.None;
                    default:
                        return CallType.UserOperator;
                }
            }

            return CallType.None;
        }


        public static bool IsDynamic(Context cx, ExpressionSyntax node)
        {
            var ti = cx.GetTypeInfo(node).ConvertedType;
            return ti != null && ti.TypeKind == Microsoft.CodeAnalysis.TypeKind.Dynamic;
        }

        /// <summary>
        /// Given b in a?.b.c, return a.
        /// </summary>
        /// <param name="node">A MemberBindingExpression.</param>
        /// <returns>The qualifier of the conditional access.</returns>
        protected static ExpressionSyntax FindConditionalQualifier(ExpressionSyntax node)
        {
            for (SyntaxNode n = node; n != null; n = n.Parent)
            {
                var conditionalAccess = n.Parent as ConditionalAccessExpressionSyntax;

                if (conditionalAccess != null && conditionalAccess.WhenNotNull == n)
                    return conditionalAccess.Expression;
            }

            throw new InternalError(node, "Unable to locate a ConditionalAccessExpression");
        }

        public void MakeConditional(TextWriter trapFile)
        {
            trapFile.conditional_access(this);
        }

        public void PopulateArguments(TextWriter trapFile, BaseArgumentListSyntax args, int child)
        {
            foreach (var arg in args.Arguments)
                PopulateArgument(trapFile, arg, child++);
        }

        private void PopulateArgument(TextWriter trapFile, ArgumentSyntax arg, int child)
        {
            var expr = Create(cx, arg.Expression, this, child);
            int mode;
            switch (arg.RefOrOutKeyword.Kind())
            {
                case SyntaxKind.RefKeyword:
                    mode = 1;
                    break;
                case SyntaxKind.OutKeyword:
                    mode = 2;
                    break;
                case SyntaxKind.None:
                    mode = 0;
                    break;
                case SyntaxKind.InKeyword:
                    mode = 3;
                    break;
                default:
                    throw new InternalError(arg, "Unknown argument type");
            }
            trapFile.expr_argument(expr, mode);

            if (arg.NameColon != null)
            {
                trapFile.expr_argument_name(expr, arg.NameColon.Name.Identifier.Text);
            }
        }

        public override string ToString() => Label.ToString();

        public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.OptionalLabel;
    }

    static class CallTypeExtensions
    {
        /// <summary>
        /// Adjust the expression kind <paramref name="k"/> to match this call type.
        /// </summary>
        public static ExprKind AdjustKind(this Expression.CallType ct, ExprKind k)
        {
            switch (ct)
            {
                case Expression.CallType.Dynamic:
                case Expression.CallType.UserOperator:
                    return ExprKind.OPERATOR_INVOCATION;
                default:
                    return k;
            }
        }
    }

    abstract class Expression<SyntaxNode> : Expression
        where SyntaxNode : ExpressionSyntax
    {
        public readonly SyntaxNode Syntax;

        protected Expression(ExpressionNodeInfo info)
            : base(info)
        {
            Syntax = (SyntaxNode)info.Node;
        }

        /// <summary>
        /// Populates expression-type specific relations in the trap file. The general relations
        /// <code>expressions</code> and <code>expr_location</code> are populated by the constructor
        /// (should not fail), so even if expression-type specific population fails (e.g., in
        /// standalone extraction), the expression created via
        /// <see cref="Expression.Create(Context, ExpressionSyntax, IEntity, int, ITypeSymbol)"/> will
        /// still be valid.
        /// </summary>
        protected abstract void PopulateExpression(TextWriter trapFile);

        protected new Expression TryPopulate()
        {
            cx.Try(Syntax, null, ()=>PopulateExpression(cx.TrapWriter.Writer));
            return this;
        }
    }

    /// <summary>
    /// Holds all information required to create an Expression entity.
    /// </summary>
    interface IExpressionInfo
    {
        Context Context { get; }

        /// <summary>
        /// The type of the expression.
        /// </summary>
        AnnotatedType Type { get; }

        /// <summary>
        /// The location of the expression.
        /// </summary>
        Extraction.Entities.Location Location { get; }

        /// <summary>
        /// The kind of the expression.
        /// </summary>
        ExprKind Kind { get; }

        /// <summary>
        /// The parent of the expression.
        /// </summary>
        IExpressionParentEntity Parent { get; }

        /// <summary>
        /// The child index of the expression.
        /// </summary>
        int Child { get; }

        /// <summary>
        /// Holds if this is an implicit expression.
        /// </summary>
        bool IsCompilerGenerated { get; }

        /// <summary>
        /// Gets a string representation of the value.
        /// null is encoded as the string "null".
        /// If the expression does not have a value, then this
        /// is null.
        /// </summary>
        string ExprValue { get; }
    }

    /// <summary>
    /// Explicitly constructed expression information.
    /// </summary>
    class ExpressionInfo : IExpressionInfo
    {
        public Context Context { get; }
        public AnnotatedType Type { get; }
        public Extraction.Entities.Location Location { get; }
        public ExprKind Kind { get; }
        public IExpressionParentEntity Parent { get; }
        public int Child { get; }
        public bool IsCompilerGenerated { get; }
        public string ExprValue { get; }

        public ExpressionInfo(Context cx, AnnotatedType type, Extraction.Entities.Location location, ExprKind kind, IExpressionParentEntity parent, int child, bool isCompilerGenerated, string value)
        {
            Context = cx;
            Type = type;
            Location = location;
            Kind = kind;
            Parent = parent;
            Child = child;
            ExprValue = value;
            IsCompilerGenerated = isCompilerGenerated;
        }
    }

    /// <summary>
    /// Expression information constructed from a syntax node.
    /// </summary>
    class ExpressionNodeInfo : IExpressionInfo
    {
        public ExpressionNodeInfo(Context cx, ExpressionSyntax node, IExpressionParentEntity parent, int child) :
            this(cx, node, parent, child, cx.GetTypeInfo(node))
        {
        }

        public ExpressionNodeInfo(Context cx, ExpressionSyntax node, IExpressionParentEntity parent, int child, TypeInfo typeInfo)
        {
            Context = cx;
            Node = node;
            Parent = parent;
            Child = child;
            TypeInfo = typeInfo;
            Conversion = cx.GetModel(node).GetConversion(node);
        }

        public Context Context { get; }
        public ExpressionSyntax Node { get; private set; }
        public IExpressionParentEntity Parent { get; set; }
        public int Child { get; set; }
        public TypeInfo TypeInfo { get; }
        public Microsoft.CodeAnalysis.CSharp.Conversion Conversion { get; }

        public AnnotatedTypeSymbol ResolvedType => new AnnotatedTypeSymbol(TypeInfo.Type.DisambiguateType(), TypeInfo.Nullability.Annotation);
        public AnnotatedTypeSymbol ConvertedType => new AnnotatedTypeSymbol(TypeInfo.ConvertedType.DisambiguateType(), TypeInfo.ConvertedNullability.Annotation);

        public AnnotatedTypeSymbol ExpressionType
        {
            get
            {
                var type = ResolvedType;

                if (type.Symbol == null)
                    type.Symbol = (TypeInfo.Type ?? TypeInfo.ConvertedType).DisambiguateType();

                // Roslyn workaround: It can't work out the type of "new object[0]"
                // Clearly a bug.
                if (type.Symbol?.TypeKind == Microsoft.CodeAnalysis.TypeKind.Error)
                {
                    var arrayCreation = Node as ArrayCreationExpressionSyntax;
                    if (arrayCreation != null)
                    {
                        var elementType = Context.GetType(arrayCreation.Type.ElementType);

                        if (elementType.Symbol != null)
                            // There seems to be no way to create an array with a nullable element at present.
                            return new AnnotatedTypeSymbol(Context.Compilation.CreateArrayTypeSymbol(elementType.Symbol, arrayCreation.Type.RankSpecifiers.Count), NullableAnnotation.NotAnnotated);
                    }

                    Context.ModelError(Node, "Failed to determine type");
                }

                return type;
            }
        }

        Microsoft.CodeAnalysis.Location location;

        public Microsoft.CodeAnalysis.Location CodeAnalysisLocation
        {
            get
            {
                if (location == null)
                    location = Node.FixedLocation();
                return location;
            }
            set
            {
                location = value;
            }
        }

        public SemanticModel Model => Context.GetModel(Node);

        public string ExprValue
        {
            get
            {
                var c = Model.GetConstantValue(Node);
                return c.HasValue ? Expression.ValueAsString(c.Value) : null;
            }
        }

        AnnotatedType cachedType;

        public AnnotatedType Type
        {
            get
            {
                if (cachedType.Type == null)
                    cachedType = Entities.Type.Create(Context, ExpressionType);
                return cachedType;
            }
            set
            {
                cachedType = value;
            }
        }

        Extraction.Entities.Location cachedLocation;

        public Extraction.Entities.Location Location
        {
            get
            {
                if (cachedLocation == null)
                    cachedLocation = Context.Create(CodeAnalysisLocation);
                return cachedLocation;
            }

            set
            {
                cachedLocation = value;
            }
        }

        public ExprKind Kind { get; set; } = ExprKind.UNKNOWN;

        public bool IsCompilerGenerated { get; set; }

        public ExpressionNodeInfo SetParent(IExpressionParentEntity parent, int child)
        {
            Parent = parent;
            Child = child;
            return this;
        }

        public ExpressionNodeInfo SetKind(ExprKind kind)
        {
            Kind = kind;
            return this;
        }

        public ExpressionNodeInfo SetType(AnnotatedType type)
        {
            Type = type;
            return this;
        }

        public ExpressionNodeInfo SetNode(ExpressionSyntax node)
        {
            Node = node;
            return this;
        }

        SymbolInfo cachedSymbolInfo;

        public SymbolInfo SymbolInfo
        {
            get
            {
                if (cachedSymbolInfo.Symbol == null && cachedSymbolInfo.CandidateReason == CandidateReason.None)
                    cachedSymbolInfo = Model.GetSymbolInfo(Node);
                return cachedSymbolInfo;
            }
        }
    }
}
