using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using Radzen;

/// <summary>
/// Serializes LINQ Expression Trees into C# string representations.
/// </summary>
[UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2026, Justification = TrimMessages.DataTypePreserved)]
public class ExpressionSerializer : ExpressionVisitor
{
    private readonly StringBuilder _sb = new StringBuilder();

    /// <summary>
    /// Serializes a given LINQ Expression into a C# string.
    /// </summary>
    /// <param name="expression">The expression to serialize.</param>
    /// <returns>A string representation of the expression.</returns>
    public string Serialize(Expression expression)
    {
        _sb.Clear();
        Visit(expression);
        return _sb.ToString();
    }

    /// <inheritdoc/>
    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        ArgumentNullException.ThrowIfNull(node);
        if (node.Parameters.Count > 1)
        {
            _sb.Append("(");
            for (int i = 0; i < node.Parameters.Count; i++)
            {
                if (i > 0)
                {
                    _sb.Append(", ");
                }

                _sb.Append(node.Parameters[i].Name);
            }
            _sb.Append(") => ");
        }
        else
        {
            _sb.Append(node.Parameters[0].Name);
            _sb.Append(" => ");
        }
        Visit(node.Body);
        return node;
    }

    /// <inheritdoc/>
    protected override Expression VisitParameter(ParameterExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        _sb.Append(node.Name);
        return node;
    }

    /// <inheritdoc/>
    protected override Expression VisitMember(MemberExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        if (node.Expression != null)
        {
            Visit(node.Expression);
            _sb.Append(CultureInfo.InvariantCulture, $".{node.Member.Name}");
        }
        else
        {
            _sb.Append(node.Member.Name);
        }
        return node;
    }

    /// <inheritdoc/>
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        if (node.Method.IsStatic && node.Arguments.Count > 0 &&
            (node.Method.DeclaringType == typeof(Enumerable) ||
                node.Method.DeclaringType == typeof(Queryable)))
        {
            Visit(node.Arguments[0]);
            _sb.Append(CultureInfo.InvariantCulture, $".{node.Method.Name}(");

            for (int i = 1; i < node.Arguments.Count; i++)
            {
                if (i > 1)
                {
                    _sb.Append(", ");
                }

                if (node.Arguments[i] is NewArrayExpression arrayExpr)
                {
                    VisitNewArray(arrayExpr);
                }
                else
                {
                    Visit(node.Arguments[i]);
                }
            }

            _sb.Append(")");
        }
        else if (node.Method.IsStatic)
        {
            _sb.Append(CultureInfo.InvariantCulture, $"{node.Method.DeclaringType?.Name}.{node.Method.Name}(");

            for (int i = 0; i < node.Arguments.Count; i++)
            {
                if (i > 0)
                {
                    _sb.Append(", ");
                }

                VisitMethodArgument(node.Arguments[i]);
            }

            _sb.Append(")");
        }
        else
        {
            if (node.Object != null)
            {
                Visit(node.Object);
                _sb.Append(CultureInfo.InvariantCulture, $".{node.Method.Name}(");
            }
            else
            {
                _sb.Append(CultureInfo.InvariantCulture, $"{node.Method.Name}(");
            }

            for (int i = 0; i < node.Arguments.Count; i++)
            {
                if (i > 0)
                {
                    _sb.Append(", ");
                }

                VisitMethodArgument(node.Arguments[i]);
            }

            _sb.Append(")");
        }

        return node;
    }

    // FormatValue serializes enums as bare numbers, but a method argument needs its enum type for the
    // parser's overload resolution (e.g. DateTime.Parse(s, provider, DateTimeStyles)). Emit the cast here
    // (not in FormatValue) so overloads still bind while comparison/array enums stay numeric.
    private void VisitMethodArgument(Expression argument)
    {
        if (argument is ConstantExpression { Value: not null } constant)
        {
            var type = Nullable.GetUnderlyingType(constant.Type) ?? constant.Type;
            if (type.IsEnum)
            {
                _sb.Append(CultureInfo.InvariantCulture, $"({type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})");
            }
        }

        Visit(argument);
    }

    /// <inheritdoc/>
    protected override Expression VisitUnary(UnaryExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        if (node.NodeType == ExpressionType.Not)
        {
            _sb.Append("(!(");
            Visit(node.Operand);
            _sb.Append("))");
        }
        else if (node.NodeType == ExpressionType.Convert)
        {
            if (node.Operand is IndexExpression indexExpr)
            {
                _sb.Append(CultureInfo.InvariantCulture, $"({node.Type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})");

                Visit(indexExpr.Object);

                _sb.Append("[");
                Visit(indexExpr.Arguments[0]);
                _sb.Append("]");

                return node;
            }

            Visit(node.Operand);
        }
        else
        {
            _sb.Append(node.NodeType switch
            {
                ExpressionType.Negate => "-",
                ExpressionType.UnaryPlus => "+",
                _ => throw new NotSupportedException($"Unsupported unary operator: {node.NodeType}")
            });
            Visit(node.Operand);
        }
        return node;
    }

    /// <inheritdoc/>
    protected override Expression VisitConstant(ConstantExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        _sb.Append(FormatValue(node.Value));
        return node;
    }

    internal static string? FormatValue(object? value)
    {
        return value switch
        {
            string s when s.Length == 0 => @"""""",
            null => "null",
            string s => @$"""{s.Replace("\"", "\\\"", StringComparison.Ordinal)}""",
            char c => $"'{c}'",
            bool b => b.ToString().ToLowerInvariant(),
            DateTime dt => FormatDateTime(dt),
            DateTimeOffset dto => $"DateTime.Parse(\"{dto.UtcDateTime:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
            DateOnly dateOnly => $"DateOnly.Parse(\"{dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
            TimeOnly timeOnly => $"TimeOnly.Parse(\"{timeOnly.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
            Guid guid => $"Guid.Parse(\"{guid.ToString("D", CultureInfo.InvariantCulture)}\")",
            IEnumerable enumerable when value is not string => FormatEnumerable(enumerable),
            // Emit the underlying numeric value (not a (Namespace.EnumType) cast): the cast forces
            // ResolveTypeFromAssemblies (AppDomain.GetTypes) on deserialization, which is trim/AOT hostile.
            // The enum type is recovered on parse from the compared member / typed array prefix.
            _ => value.GetType().IsEnum
                ? Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString()
                : Convert.ToString(value, CultureInfo.InvariantCulture)
        };
    }

    private static string FormatDateTime(DateTime dateTime)
    {
        var finalDate = dateTime.TimeOfDay == TimeSpan.Zero ? dateTime.Date : dateTime;
        var dateFormat = dateTime.TimeOfDay == TimeSpan.Zero ? "yyyy-MM-dd" : "yyyy-MM-ddTHH:mm:ss.fffZ";

        return $"DateTime.SpecifyKind(DateTime.Parse(\"{finalDate.ToString(dateFormat, CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), DateTimeKind.{Enum.GetName(finalDate.Kind)})";
    }

    private static string FormatEnumerable(IEnumerable enumerable)
    {
        var arrayType = enumerable.AsQueryable().ElementType;

        // Enum elements are serialized as bare numbers (see FormatValue); emit the typed array prefix so the
        // parser binds the array as the enum type (required for Intersect/Except). Nullable elements already
        // need the prefix to carry the Nullable<> wrapper.
        var underlying = Nullable.GetUnderlyingType(arrayType) ?? arrayType;
        var needsTypePrefix = Nullable.GetUnderlyingType(arrayType) != null || underlying.IsEnum;

        var items = enumerable.Cast<object>().Select(FormatValue);
        return $"new {(needsTypePrefix ? arrayType.DisplayName(true).Replace("+", ".", StringComparison.Ordinal) : "")}[] {{ {string.Join(", ", items)} }}";
    }

    /// <inheritdoc/>
    protected override Expression VisitNewArray(NewArrayExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        bool needsParentheses = node.NodeType == ExpressionType.NewArrayInit &&
                                (node.Expressions.Count > 1 || node.Expressions[0].NodeType != ExpressionType.Constant);

        if (needsParentheses)
        {
            _sb.Append("(");
        }

        // Enum elements serialize as bare numbers (see FormatValue); emit the typed array prefix so the
        // parser binds the array as the enum type (required for Intersect/Except) instead of int[].
        var elementType = node.Type.GetElementType();
        var underlying = elementType != null ? Nullable.GetUnderlyingType(elementType) ?? elementType : null;
        var typePrefix = underlying != null && (underlying.IsEnum || Nullable.GetUnderlyingType(elementType!) != null)
            ? elementType!.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)
            : "";

        _sb.Append(CultureInfo.InvariantCulture, $"new {typePrefix}[] {{ ");
        bool first = true;
        foreach (var expr in node.Expressions)
        {
            if (!first)
            {
                _sb.Append(", ");
            }

            first = false;
            Visit(expr);
        }
        _sb.Append(" }");

        if (needsParentheses)
        {
            _sb.Append(")");
        }

        return node;
    }

    /// <inheritdoc/>
    protected override Expression VisitBinary(BinaryExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        _sb.Append("(");
        Visit(node.Left);
        _sb.Append(CultureInfo.InvariantCulture, $" {GetOperator(node.NodeType)} ");
        Visit(node.Right);
        _sb.Append(")");
        return node;
    }

    /// <inheritdoc/>
    protected override Expression VisitConditional(ConditionalExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        _sb.Append("(");
        Visit(node.Test);
        _sb.Append(" ? ");
        Visit(node.IfTrue);
        _sb.Append(" : ");
        Visit(node.IfFalse);
        _sb.Append(")");
        return node;
    }

    /// <inheritdoc/>
    protected override Expression VisitDefault(DefaultExpression node)
    {
        ArgumentNullException.ThrowIfNull(node);
        if (!node.Type.IsValueType || Nullable.GetUnderlyingType(node.Type) != null)
        {
            _sb.Append("null");
        }
        else
        {
            _sb.Append(CultureInfo.InvariantCulture, $"default({node.Type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})");
        }
        return node;
    }

    /// <summary>
    /// Maps an ExpressionType to its corresponding C# operator.
    /// </summary>
    /// <param name="type">The ExpressionType to map.</param>
    /// <returns>A string representation of the corresponding C# operator.</returns>
    private static string GetOperator(ExpressionType type)
    {
        return type switch
        {
            ExpressionType.Add => "+",
            ExpressionType.Subtract => "-",
            ExpressionType.Multiply => "*",
            ExpressionType.Divide => "/",
            ExpressionType.AndAlso => "&&",
            ExpressionType.OrElse => "||",
            ExpressionType.Equal => "==",
            ExpressionType.NotEqual => "!=",
            ExpressionType.LessThan => "<",
            ExpressionType.LessThanOrEqual => "<=",
            ExpressionType.GreaterThan => ">",
            ExpressionType.GreaterThanOrEqual => ">=",
            ExpressionType.Coalesce => "??",
            _ => throw new NotSupportedException($"Unsupported operator: {type}")
        };
    }
}

/// <summary>
/// Provides an extension method for displaying type names.
/// </summary>
public static class SharedTypeExtensions
{
    private static readonly Dictionary<Type, string> BuiltInTypeNames = new()
    {
        { typeof(bool), "bool" },
        { typeof(byte), "byte" },
        { typeof(char), "char" },
        { typeof(decimal), "decimal" },
        { typeof(double), "double" },
        { typeof(float), "float" },
        { typeof(int), "int" },
        { typeof(long), "long" },
        { typeof(object), "object" },
        { typeof(sbyte), "sbyte" },
        { typeof(short), "short" },
        { typeof(string), "string" },
        { typeof(uint), "uint" },
        { typeof(ulong), "ulong" },
        { typeof(ushort), "ushort" },
        { typeof(void), "void" }
    };

    /// <summary>
    /// Unwraps nullable type.
    /// </summary>
    public static Type UnwrapNullableType(this Type type)
        => Nullable.GetUnderlyingType(type) ?? type;

    /// <summary>
    /// Returns a display name for the given type.
    /// </summary>
    /// <param name="type">The type to display.</param>
    /// <param name="fullName">Indicates whether to use the full name.</param>
    /// <param name="compilable">Indicates whether to use a compilable format.</param>
    /// <returns>A string representing the type name.</returns>
    public static string DisplayName(this Type type, bool fullName = true, bool compilable = false)
    {
        ArgumentNullException.ThrowIfNull(type);
        var stringBuilder = new StringBuilder();
        ProcessType(stringBuilder, type, fullName, compilable);
        return stringBuilder.ToString();
    }

    private static void ProcessType(StringBuilder builder, Type type, bool fullName, bool compilable)
    {
        if (type.IsGenericType)
        {
            var genericArguments = type.GetGenericArguments();
            ProcessGenericType(builder, type, genericArguments, genericArguments.Length, fullName, compilable);
        }
        else if (type.IsArray)
        {
            ProcessArrayType(builder, type, fullName, compilable);
        }
        else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
        {
            builder.Append(builtInName);
        }
        else if (!type.IsGenericParameter)
        {
            if (compilable)
            {
                if (type.IsNested)
                {
                    ProcessType(builder, type.DeclaringType!, fullName, compilable);
                    builder.Append('.');
                }
                else if (fullName)
                {
                    builder.Append(type.Namespace).Append('.');
                }

                builder.Append(type.Name);
            }
            else
            {
                builder.Append(fullName ? type.FullName : type.Name);
            }
        }
    }

    private static void ProcessArrayType(StringBuilder builder, Type type, bool fullName, bool compilable)
    {
        var innerType = type;
        while (innerType.IsArray)
        {
            innerType = innerType.GetElementType()!;
        }

        ProcessType(builder, innerType, fullName, compilable);

        while (type.IsArray)
        {
            builder.Append('[');
            builder.Append(',', type.GetArrayRank() - 1);
            builder.Append(']');
            type = type.GetElementType()!;
        }
    }

    private static void ProcessGenericType(
        StringBuilder builder,
        Type type,
        Type[] genericArguments,
        int length,
        bool fullName,
        bool compilable)
    {
        if (type.IsConstructedGenericType
            && type.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            ProcessType(builder, type.UnwrapNullableType(), fullName, compilable);
            builder.Append('?');
            return;
        }

        var offset = type.IsNested ? type.DeclaringType!.GetGenericArguments().Length : 0;

        if (compilable)
        {
            if (type.IsNested)
            {
                ProcessType(builder, type.DeclaringType!, fullName, compilable);
                builder.Append('.');
            }
            else if (fullName)
            {
                builder.Append(type.Namespace);
                builder.Append('.');
            }
        }
        else
        {
            if (fullName)
            {
                if (type.IsNested)
                {
                    ProcessGenericType(builder, type.DeclaringType!, genericArguments, offset, fullName, compilable);
                    builder.Append('+');
                }
                else
                {
                    builder.Append(type.Namespace);
                    builder.Append('.');
                }
            }
        }

        var genericPartIndex = type.Name.IndexOf('`', StringComparison.Ordinal);
        if (genericPartIndex <= 0)
        {
            builder.Append(type.Name);
            return;
        }

        builder.Append(type.Name, 0, genericPartIndex);
        builder.Append('<');

        for (var i = offset; i < length; i++)
        {
            ProcessType(builder, genericArguments[i], fullName, compilable);
            if (i + 1 == length)
            {
                continue;
            }

            builder.Append(',');
            if (!genericArguments[i + 1].IsGenericParameter)
            {
                builder.Append(' ');
            }
        }

        builder.Append('>');
    }
}