const std = @import("std");
const ast = @import("../ast.zig");
const types = @import("types.zig");
const lower = @import("lower.zig");

const Node = ast.Node;
const TypeId = types.TypeId;
const Lowering = lower.Lowering;

/// Coercion planning (architecture phase A4.3): classify HOW a value of one
/// type converts to another, before `Lowering` emits the IR for it. The
/// classifier is pure (reads the type table + protocol/impl registries); all
/// actual IR emission — `unbox_any`/`optional_wrap`/`int_to_float`/protocol
/// erasure/the `Into` call — stays in `Lowering`.
///
/// A `*Lowering` facade (Principle 5, like `CallResolver`/`GenericResolver`/
/// `ProtocolResolver`). Two entry points:
///   - `classify(src, dst)` — the built-in coercion ladder consumed by
///     `coerceToType` (the shared, recursive value-conversion path).
///   - `classifyXX(src, dst)` — the `xx`-operator head consumed by `lowerXX`
///     (Any unbox, no-op, protocol erasure, protocol→pointer, else the ladder
///     + the user-`Into` fallback).
pub const CoercionResolver = struct {
    l: *Lowering,

    /// The built-in coercion the `coerceToType` ladder will emit for `src → dst`.
    /// `.none` means no built-in applies (the value passes through unchanged;
    /// `lowerXX` then tries a user `Into`). Branch order mirrors `coerceToType`
    /// exactly — the emitter switches on this and reproduces each arm.
    pub const CoercionPlan = enum {
        no_op, // src == dst
        unbox_any, // any → concrete
        box_any, // concrete → any
        closure_to_fn_reject, // closure value → bare fn-ptr (diagnostic, returns operand)
        tuple_elementwise, // (A,B) → (C,D), same arity
        optional_unwrap, // ?T → concrete (narrowing)
        void_to_optional, // void (null literal) → ?T
        optional_wrap, // concrete → ?T
        erase_protocol, // concrete → protocol value
        int_to_float,
        float_to_int,
        ptr_int_bitcast, // ptr ↔ int
        widen, // same kind, dst wider
        narrow, // same kind, dst narrower
        none, // nothing applies — pass the value through
    };

    pub fn classify(self: CoercionResolver, src_ty: TypeId, dst_ty: TypeId) CoercionPlan {
        if (src_ty == dst_ty) return .no_op;
        if (src_ty == .any and dst_ty != .any) return .unbox_any;
        if (dst_ty == .any and src_ty != .any) return .box_any;

        if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
            if (self.l.module.types.get(src_ty) == .closure and self.l.module.types.get(dst_ty) == .function) {
                return .closure_to_fn_reject;
            }
        }

        // Tuple → Tuple, same arity.
        if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
            const si = self.l.module.types.get(src_ty);
            const di = self.l.module.types.get(dst_ty);
            if (si == .tuple and di == .tuple and si.tuple.fields.len == di.tuple.fields.len) {
                return .tuple_elementwise;
            }
        }

        // Optional → Concrete unwrap (narrowing).
        if (!src_ty.isBuiltin()) {
            const src_info = self.l.module.types.get(src_ty);
            if (src_info == .optional) {
                const child_ty = src_info.optional.child;
                if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) {
                    return .optional_unwrap;
                }
            }
        }

        // void (null literal) → Optional.
        if (src_ty == .void and !dst_ty.isBuiltin()) {
            if (self.l.module.types.get(dst_ty) == .optional) return .void_to_optional;
        }

        // Concrete → Optional wrap.
        if (!dst_ty.isBuiltin()) {
            if (self.l.module.types.get(dst_ty) == .optional) return .optional_wrap;
        }

        // Concrete → Protocol (auto type erasure) — only when the source has a
        // resolvable concrete type name; otherwise fall through to the numeric
        // ladder (matching `coerceToType`, which leaves the erase block).
        if (self.l.getProtocolInfo(dst_ty) != null) {
            if (self.l.resolveConcreteTypeName(src_ty) != null) return .erase_protocol;
        }

        // Numeric / pointer ladder.
        const src_float = Lowering.isFloat(src_ty);
        const dst_float = Lowering.isFloat(dst_ty);
        const src_int = self.l.isIntEx(src_ty);
        const dst_int = self.l.isIntEx(dst_ty);
        const src_ptr = !src_ty.isBuiltin() and self.l.module.types.get(src_ty) == .pointer;
        const dst_ptr = !dst_ty.isBuiltin() and self.l.module.types.get(dst_ty) == .pointer;

        if (src_int and dst_float) return .int_to_float;
        if (src_float and dst_int) return .float_to_int;
        if ((src_ptr and dst_int) or (src_int and dst_ptr)) return .ptr_int_bitcast;

        const src_bits = self.l.typeBitsEx(src_ty);
        const dst_bits = self.l.typeBitsEx(dst_ty);
        if (src_bits > 0 and dst_bits > 0) {
            if (dst_bits < src_bits) return .narrow;
            if (dst_bits > src_bits) return .widen;
        }
        return .none;
    }

    /// The `xx`-operator head decision for `lowerXX`. `.coerce` defers to the
    /// built-in ladder (`coerceToType` / `classify`) + the user-`Into` fallback.
    /// Branch order mirrors `lowerXX` exactly.
    pub const XXPlan = enum {
        unbox_any, // src is Any → unbox (lowerXX adds the f32/f64 match dispatch)
        no_op, // src == dst
        erase_protocol, // dst is a protocol → buildProtocolErasure
        protocol_to_pointer, // src is a protocol, dst is a pointer → recover ctx
        coerce, // built-in ladder + user `Into` fallback
    };

    pub fn classifyXX(self: CoercionResolver, src_ty: TypeId, dst_ty: TypeId) XXPlan {
        if (src_ty == .any) return .unbox_any;
        if (src_ty == dst_ty) return .no_op;
        if (self.l.getProtocolInfo(dst_ty) != null) return .erase_protocol;
        if (self.l.getProtocolInfo(src_ty) != null and !dst_ty.isBuiltin() and
            self.l.module.types.get(dst_ty) == .pointer) return .protocol_to_pointer;
        return .coerce;
    }
};
