using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.CSharp.Entities.Expressions;
using Semmle.Extraction.Kinds;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Semmle.Extraction.CSharp.Entities
{
    internal class Expression : FreshEntity, IExpressionParentEntity
    {
        private readonly IExpressionInfo info;
        public AnnotatedTypeSymbol? Type { get; private set; }
        public Extraction.Entities.Location Location { get; }
        public ExprKind Kind { get; }

        internal Expression(IExpressionInfo info, bool shouldPopulate = true)
            : base(info.Context)
        {
            this.info = info;
            Location = info.Location;
            Kind = info.Kind;
            Type = info.Type;

            if (shouldPopulate)
            {
                TryPopulate();
            }
        }

        protected sealed override void Populate(TextWriter trapFile)
        {
            var type = Type.HasValue ? Entities.Type.Create(Context, Type.Value) : NullType.Create(Context);
            trapFile.expressions(this, Kind, 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 (Type.HasValue && !Type.Value.HasObliviousNullability())
            {
                var n = NullabilityEntity.Create(Context, Nullability.Create(Type.Value));
                trapFile.type_nullability(this, n);
            }

            if (info.FlowState != NullableFlowState.None)
            {
                trapFile.expr_flowstate(this, (int)info.FlowState);
            }

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

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

            type.PopulateGenerics();
        }

        public override Location? ReportingLocation => Location.Symbol;

        internal void SetType(ITypeSymbol? type)
        {
            if (type is not null)
            {
                Type = new AnnotatedTypeSymbol(type, type.NullableAnnotation);
            }
        }

        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 is null
                ? "null"
                : value is bool b
                    ? b
                        ? "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));
        }

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

        /// <summary>
        /// Creates a generated expression from a typed constant.
        /// </summary>
        public static Expression? CreateGenerated(Context cx, TypedConstant constant, IExpressionParentEntity parent,
            int childIndex, Extraction.Entities.Location location)
        {
            if (constant.IsNull ||
                constant.Type is null)
            {
                return Literal.CreateGeneratedNullLiteral(cx, parent, childIndex, location);
            }

            switch (constant.Kind)
            {
                case TypedConstantKind.Primitive:
                    return Literal.CreateGenerated(cx, parent, childIndex, constant.Type, constant.Value, location);
                case TypedConstantKind.Enum:
                    // Enum value is generated in the following format: (Enum)value
                    Action<Expression, int> createChild = (parent, index) => Literal.CreateGenerated(cx, parent, index, ((INamedTypeSymbol)constant.Type).EnumUnderlyingType!, constant.Value, location);
                    var cast = Cast.CreateGenerated(cx, parent, childIndex, constant.Type!, constant.Value, createChild, location);
                    return cast;
                case TypedConstantKind.Type:
                    var type = ((ITypeSymbol)constant.Value!).OriginalDefinition;
                    return TypeOf.CreateGenerated(cx, parent, childIndex, type, location);
                case TypedConstantKind.Array:
                    // Single dimensional arrays are in the following format:
                    // * new Type[N] { item1, item2, ..., itemN }
                    // * new Type[0]
                    //
                    // itemI is generated recursively.
                    return NormalArrayCreation.CreateGenerated(cx, parent, childIndex, constant.Type, constant.Values, location);
                case TypedConstantKind.Error:
                default:
                    cx.ExtractionError("Couldn't extract constant in attribute", constant.ToString(), location);
                    return null;
            }
        }

        /// <summary>
        /// Creates a generated expression for a default argument value.
        /// </summary>
        public static Expression? CreateGenerated(Context cx, IParameterSymbol parameter, IExpressionParentEntity parent,
            int childIndex, Extraction.Entities.Location location)
        {
            if (!parameter.HasExplicitDefaultValue ||
                parameter.Type is IErrorTypeSymbol)
            {
                return null;
            }

            var defaultValue = parameter.ExplicitDefaultValue;

            var type = parameter.Type;
            if (type.IsBoundNullable() && type is INamedTypeSymbol named)
            {
                type = named.TypeArguments[0];
            }

            if (type is INamedTypeSymbol nt && nt.EnumUnderlyingType is not null)
            {
                // = (MyEnum)1, = MyEnum.Value1, = default(MyEnum), = new MyEnum()
                // we're generating a (MyEnum)value cast expression:
                defaultValue ??= 0;
                Action<Expression, int> createChild = (parent, index) => Literal.CreateGenerated(cx, parent, index, nt.EnumUnderlyingType, defaultValue, location);
                return Cast.CreateGenerated(cx, parent, childIndex, parameter.Type, defaultValue, createChild, location);
            }

            if (defaultValue is null)
            {
                // = null, = default, = default(T), = new MyStruct()
                // we're generating a default expression:
                return Default.CreateGenerated(cx, parent, childIndex, location, parameter.Type.IsReferenceType ? ValueAsString(null) : null);
            }

            if (parameter.Type.SpecialType is SpecialType.System_Object)
            {
                // this can happen in VB.NET
                cx.ExtractionError($"Extracting default argument value 'object {parameter.Name} = default' instead of 'object {parameter.Name} = {defaultValue}'. The latter is not supported in C#.",
                    null, null, severity: Util.Logging.Severity.Warning);

                // we're generating a default expression:
                return Default.CreateGenerated(cx, parent, childIndex, location, ValueAsString(null));
            }

            // const literal:
            return Literal.CreateGenerated(cx, parent, childIndex, type, defaultValue, location);
        }

        /// <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 = Context.GetSymbolInfo(node);
            if (@operator.Symbol is IMethodSymbol method)
            {
                var callType = GetCallType(Context, node);
                if (callType == CallType.Dynamic)
                {
                    UserOperator.OperatorSymbol(method.Name, out var operatorName);
                    trapFile.dynamic_member_name(this, operatorName);
                    return;
                }

                trapFile.expr_call(this, Method.Create(Context, 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 is not 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 is not 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 is not null; n = n.Parent)
            {
                if (n.Parent is ConditionalAccessExpressionSyntax conditionalAccess &&
                    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)
        {
            PopulateArguments(trapFile, args.Arguments, child);
        }

        public void PopulateArguments(TextWriter trapFile, IEnumerable<ArgumentSyntax> args, int child)
        {
            foreach (var arg in args)
                PopulateArgument(trapFile, arg, child++);
        }

        private void PopulateArgument(TextWriter trapFile, ArgumentSyntax arg, int child)
        {
            var expr = Create(Context, 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 is not null)
            {
                trapFile.expr_argument_name(expr, arg.NameColon.Name.Identifier.Text);
            }
        }

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

        public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.OptionalLabel;
    }
}
