Editor/Projection/CSharp/Projectors/ExpressionEmitter.cs
using System.Linq;
using System.Text.Json;
using Grains.RazorDesigner.Wiring;

namespace Grains.RazorDesigner.Projection.CSharp.Projectors;

public static class ExpressionEmitter
{
    public static string Emit( Expression expr, CSharpProjectorContext ctx )
    {
        switch ( expr )
        {
            case LiteralExpression lit:        return EmitLiteral( lit );
            case SymbolRefExpression sref:     return EmitSymbolRef( sref, ctx );
            case BinaryOpExpression bop:       return EmitBinaryOp( bop, ctx );
            case UnaryOpExpression uop:        return EmitUnaryOp( uop, ctx );
            case ConditionalExpression cond:   return EmitConditional( cond, ctx );
            case MethodCallExpression mc:      return EmitMethodCall( mc, ctx );
            case MemberAccessExpression ma:    return EmitMemberAccess( ma, ctx );
            case InlineExpression inl:         return inl.Code ?? "";
            default:
                throw new System.Diagnostics.UnreachableException(
                    $"ExpressionEmitter: unhandled Expression variant '{expr?.GetType().Name}'. " +
                    "Add an arm here AND add the leaf to Expression.cs's JsonDerivedType list in the same commit." );
        }
    }

    // Public so ActionEmitter (Task 4) can call it for CallAction's callee without duplication.
    public static string EmitCallee( CalleeRef callee, CSharpProjectorContext ctx )
    {
        switch ( callee )
        {
            case MethodCallee mc:
            {
                if ( ctx.Symbols.TryGetValue( mc.SymbolId, out var symbol ) )
                {
                    if ( symbol is MethodSymbol )
                        return symbol.Name;
                    return symbol.Name + " /* not-a-method */";
                }
                return $"/* missing callee {mc.SymbolId} */";
            }
            case InlineCallee ic:
                return ic.Code ?? "";
            case null:
                return "/* null callee */";
            default:
                throw new System.Diagnostics.UnreachableException(
                    $"ExpressionEmitter.EmitCallee: unhandled CalleeRef variant '{callee?.GetType().Name}'." );
        }
    }


    private static string EmitLiteral( LiteralExpression lit )
    {
        // TypeId lives on the base Expression record, read from lit directly (inherited property).
        switch ( lit.TypeId )
        {
            case "string":
            {
                if ( lit.Value.ValueKind == JsonValueKind.Null )
                    return "null";
                var raw = lit.Value.ValueKind == JsonValueKind.Undefined ? null : lit.Value.GetString();
                return "\"" + EscapeStringLiteral( raw ) + "\"";
            }
            case "int":
                return lit.Value.GetInt32().ToString( System.Globalization.CultureInfo.InvariantCulture );
            case "float":
                return lit.Value.GetDouble().ToString( "R", System.Globalization.CultureInfo.InvariantCulture ) + "f";
            case "bool":
                return lit.Value.GetBoolean() ? "true" : "false";
            default:
                return lit.Value.ValueKind == JsonValueKind.Undefined ? "" : lit.Value.ToString();
        }
    }

    private static string EmitSymbolRef( SymbolRefExpression sref, CSharpProjectorContext ctx )
    {
        if ( ctx.Symbols.TryGetValue( sref.SymbolId, out var symbol ) )
            return symbol.Name;

        return $"/* missing symbol {sref.SymbolId} */";
    }

    private static string EmitBinaryOp( BinaryOpExpression bop, CSharpProjectorContext ctx )
    {
        return "(" + Emit( bop.Left, ctx ) + " " + BinaryOpToken( bop.Op ) + " " + Emit( bop.Right, ctx ) + ")";
    }

    private static string EmitUnaryOp( UnaryOpExpression uop, CSharpProjectorContext ctx )
    {
        return "(" + UnaryOpToken( uop.Op ) + Emit( uop.Operand, ctx ) + ")";
    }

    private static string EmitConditional( ConditionalExpression cond, CSharpProjectorContext ctx )
    {
        return "(" + Emit( cond.Condition, ctx ) + " ? " + Emit( cond.Then, ctx ) + " : " + Emit( cond.Else, ctx ) + ")";
    }

    private static string EmitMethodCall( MethodCallExpression mc, CSharpProjectorContext ctx )
    {
        string prefix = "";
        if ( mc.Await )
        {
            ctx.RecordAwait();
            prefix = "await ";
        }

        var arglist = string.Join( ", ", mc.Args.Select( a => Emit( a, ctx ) ) );
        return prefix + EmitCallee( mc.Callee, ctx ) + "(" + arglist + ")";
    }

    private static string EmitMemberAccess( MemberAccessExpression ma, CSharpProjectorContext ctx )
    {
        // Trust the member name — the validator's surface; we don't need to escape it.
        return Emit( ma.Receiver, ctx ) + "." + ma.Member;
    }


    private static string EscapeStringLiteral( string s )
    {
        if ( s is null || s.Length == 0 ) return "";
        return s
            .Replace( "\\", "\\\\" )
            .Replace( "\"", "\\\"" )
            .Replace( "\n", "\\n" )
            .Replace( "\r", "\\r" )
            .Replace( "\t", "\\t" );
    }

    private static string BinaryOpToken( BinaryOp op )
    {
        switch ( op )
        {
            case BinaryOp.Eq:    return "==";
            case BinaryOp.NotEq: return "!=";
            case BinaryOp.Lt:    return "<";
            case BinaryOp.LtEq:  return "<=";
            case BinaryOp.Gt:    return ">";
            case BinaryOp.GtEq:  return ">=";
            case BinaryOp.And:   return "&&";
            case BinaryOp.Or:    return "||";
            case BinaryOp.Add:   return "+";
            case BinaryOp.Sub:   return "-";
            case BinaryOp.Mul:   return "*";
            case BinaryOp.Div:   return "/";
            case BinaryOp.Mod:   return "%";
            default:
                throw new System.Diagnostics.UnreachableException(
                    $"ExpressionEmitter.BinaryOpToken: unhandled BinaryOp variant '{op}'. " +
                    "Add a token arm here whenever a new BinaryOp value is added." );
        }
    }

    private static string UnaryOpToken( UnaryOp op )
    {
        switch ( op )
        {
            case UnaryOp.Not:    return "!";
            case UnaryOp.Negate: return "-";
            default:
                throw new System.Diagnostics.UnreachableException(
                    $"ExpressionEmitter.UnaryOpToken: unhandled UnaryOp variant '{op}'. " +
                    "Add a token arm here whenever a new UnaryOp value is added." );
        }
    }
}