//! Xenos → WGSL direct translator (P7).
//!
//! Replaces the runtime uber-shader interpreter (P3b/P3c) for shaders whose
//! feature set we cover. Emits a *standalone* WGSL module per shader
//! instead of walking a ucode buffer at draw time — pipeline compilation
//! happens once, then every subsequent dispatch is a direct `draw()`.
//!
//! The translator is deliberately narrow: when it encounters an opcode /
//! fetch format / CF shape it doesn't know, it returns [`None`] and the
//! caller falls back to the interpreter. This keeps the op-coverage work
//! incremental — future commits can add one opcode at a time without
//! invalidating the scaffolding.
//!
//! Current coverage (v1):
//!  * Linear CF: `Exec`/`ExecEnd`, `Alloc`, `Exit`. No loops / branches /
//!    calls / predicate-gated clauses.
//!  * ALU vector: `ADD`, `MUL`, `MAX`, `MIN`, `MAD`, `DP4`, `DP3`,
//!    `DP2_ADD`, `SEQ`, `SGT`, `SGE`, `SNE`, `FRC`, `FLOOR`.
//!  * ALU scalar: `ADDS`, `MULS`, `MAXS`, `MINS`, `RCP`, `RETAIN_PREV`.
//!  * Vertex fetch: `R32G32B32A32_FLOAT` only.
//!  * Texture fetch: 2D via the single `@group(1)` slot (same one P5/M6
//!    binds).
//!  * Exports: VS writes position + interpolator 0 (color); PS writes
//!    color0.
//!
//! When a shader exceeds this subset, [`translate`] returns `None` and
//! `gpu.shader.translate_reject{reason}` is bumped by the caller.

use crate::ucode::alu::{decode_alu, sop, vop, AluInstruction};
use crate::ucode::control_flow::{AllocKind, ControlFlowInstruction};
use crate::ucode::fetch::{decode_fetch, FetchInstruction};
use crate::ucode::ParsedShader;

/// Shader stage we're emitting for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
    Vertex,
    Pixel,
}

/// Success or refusal from the translator. On refusal, the caller falls
/// back to the runtime uber-shader interpreter.
#[derive(Debug)]
pub enum Translation {
    /// The emitted WGSL body for *this stage only*. Both VS + PS get
    /// wrapped into one module via [`combine_stages`].
    Ok(String),
    /// Translator saw an op/pattern it doesn't handle; fallback.
    Reject(&'static str),
}

/// Full WGSL module for a (VS, PS) pair ready to hand to
/// `wgpu::Device::create_shader_module`. Shares the header across the two
/// bodies so bindings, struct declarations, and helpers aren't duplicated.
pub fn combine_stages(vs_body: &str, ps_body: &str) -> String {
    let mut out = String::with_capacity(4096 + vs_body.len() + ps_body.len());
    out.push_str(MODULE_HEADER);
    out.push_str(vs_body);
    out.push_str(ps_body);
    out
}

/// Translate a single shader stage. Returns `None` on any unsupported
/// feature with a short reason string that the caller plumbs into the
/// `gpu.shader.translate_reject{reason}` metric.
pub fn translate(parsed: &ParsedShader, stage: Stage) -> Translation {
    let mut ctx = EmitCtx::new(stage);
    // Emit the stage entry function body.
    if let Err(reason) = ctx.emit_stage_body(parsed) {
        return Translation::Reject(reason);
    }
    Translation::Ok(ctx.finish())
}

/// Reject reasons; kept as static &'str for zero-alloc metrics.
pub mod reject {
    pub const VEC_OP_UNSUPPORTED: &str = "vec_op_unsupported";
    pub const SCL_OP_UNSUPPORTED: &str = "scl_op_unsupported";
    pub const CF_LOOP: &str = "cf_loop";
    pub const CF_COND: &str = "cf_cond";
    pub const CF_CALL: &str = "cf_call";
    pub const CF_UNKNOWN: &str = "cf_unknown";
    pub const VFETCH_FMT: &str = "vfetch_fmt";
    pub const TFETCH_NON2D: &str = "tfetch_non2d";
    pub const INSTR_OOB: &str = "instr_oob";
}

/// Shader-module preamble (bindings, helpers, struct defs). The bindings
/// mirror the xenos pipeline's `@group(0)` + `@group(1)` layout from P5/M6
/// so we can use **the same bind-group slots** — only the pipeline object
/// differs between interpreter mode and translator mode.
const MODULE_HEADER: &str = r#"
struct XenosDrawConstants {
    draw_index: u32,
    vertex_count: u32,
    prim_kind: u32,
    vertex_base_dwords: u32,
};

struct XenosConstants {
    alu:         array<vec4<f32>, 512>,
    fetch:       array<u32, 256>,
    bool_consts: array<u32, 8>,
    loop_consts: array<u32, 32>,
};

@group(0) @binding(0) var<uniform>       draw_ctx      : XenosDrawConstants;
@group(0) @binding(1) var<storage, read> xenos_consts  : XenosConstants;
@group(0) @binding(2) var<storage, read> vs_ucode      : array<u32>;
@group(0) @binding(3) var<storage, read> ps_ucode      : array<u32>;
@group(0) @binding(4) var<storage, read> vertex_buffer : array<u32>;

@group(1) @binding(0) var xenos_tex  : texture_2d<f32>;
@group(1) @binding(1) var xenos_samp : sampler;

struct VsOut {
    @builtin(position) position: vec4<f32>,
    @location(0) color: vec4<f32>,
};

struct FsOut {
    @location(0) color0: vec4<f32>,
};

// Helper: reciprocal guarded against divide-by-zero.
fn xe_rcp(x: f32) -> f32 {
    return select(0.0, 1.0 / x, x != 0.0);
}

// GPUBUG-102: per-format byte-swap matching canary's `GpuSwapInline`
// (xenos.h:1090-1109). Xbox 360 vertex data is big-endian; the host is
// little-endian. The fetch constant's `endian` field (low 2 bits of
// dword_1) selects:
//   0 (kNone)    — no swap
//   1 (k8in16)   — swap bytes within halfwords
//   2 (k8in32)   — full byte reverse
//   3 (k16in32)  — swap halfwords
fn gpu_swap(value: u32, endian: u32) -> u32 {
    switch endian {
        case 1u: { return ((value << 8u) & 0xFF00FF00u) | ((value >> 8u) & 0x00FF00FFu); }
        case 2u: {
            return ((value & 0x000000FFu) << 24u)
                 | ((value & 0x0000FF00u) << 8u)
                 | ((value & 0x00FF0000u) >> 8u)
                 | ((value & 0xFF000000u) >> 24u);
        }
        case 3u: { return ((value >> 16u) & 0xFFFFu) | (value << 16u); }
        default: { return value; }
    }
}
"#;

struct EmitCtx {
    stage: Stage,
    out: String,
    indent: usize,
}

impl EmitCtx {
    fn new(stage: Stage) -> Self {
        Self {
            stage,
            out: String::with_capacity(2048),
            indent: 0,
        }
    }

    fn finish(self) -> String {
        self.out
    }

    fn push(&mut self, s: &str) {
        for _ in 0..self.indent {
            self.out.push_str("    ");
        }
        self.out.push_str(s);
        self.out.push('\n');
    }

    fn emit_stage_body(&mut self, parsed: &ParsedShader) -> Result<(), &'static str> {
        // Entry function + struct header.
        match self.stage {
            Stage::Vertex => {
                self.push("@vertex");
                self.push("fn vs_main(@builtin(vertex_index) vidx: u32) -> VsOut {");
            }
            Stage::Pixel => {
                self.push("@fragment");
                self.push("fn fs_main(in: VsOut) -> FsOut {");
            }
        }
        self.indent = 1;
        // Register file + ps chain + export slots. All local `var`s so each
        // invocation gets its own state; translator-emitted code doesn't
        // need `var<private>` because we don't share across function calls.
        self.push("var r: array<vec4<f32>, 128>;");
        self.push("for (var i = 0u; i < 128u; i = i + 1u) { r[i] = vec4<f32>(0.0); }");
        self.push("var ps: f32 = 0.0;");
        match self.stage {
            Stage::Vertex => {
                // Seed r0 with vertex index for simple shaders that read it.
                self.push("r[0] = vec4<f32>(f32(vidx), 0.0, 0.0, 1.0);");
                // Synthetic export slots — match the interpreter's layout so
                // the fallback path and translator path produce the same
                // visual output on shaders both support.
                self.push("var opos: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 1.0);");
                self.push("var ocolor: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0);");
            }
            Stage::Pixel => {
                // Seed r0.xy with interpolated color lane so trivial shaders
                // that read r0 still produce something.
                self.push("r[0] = in.color;");
                self.push("var ocolor0: vec4<f32> = in.color;");
            }
        }

        let mut current_alloc = AllocKind::Other;
        for clause in &parsed.cf {
            match clause {
                ControlFlowInstruction::Exec {
                    address,
                    count,
                    sequence,
                    is_end,
                    predicated,
                    ..
                } => {
                    if *predicated {
                        return Err(reject::CF_COND);
                    }
                    self.emit_exec(parsed, *address, *count, *sequence, current_alloc)?;
                    if *is_end {
                        break;
                    }
                }
                ControlFlowInstruction::Alloc { kind, .. } => {
                    current_alloc = *kind;
                }
                ControlFlowInstruction::Exit => break,
                // Non-executing CF clauses: padding (`kNop`) and the
                // vertex-fetch-done hint (`kMarkVsFetchDone`). Skip them.
                ControlFlowInstruction::Nop
                | ControlFlowInstruction::MarkVsFetchDone => {}
                ControlFlowInstruction::LoopStart { .. }
                | ControlFlowInstruction::LoopEnd { .. } => return Err(reject::CF_LOOP),
                ControlFlowInstruction::CondJmp { .. } => return Err(reject::CF_COND),
                ControlFlowInstruction::CondCall { .. } | ControlFlowInstruction::Return => {
                    return Err(reject::CF_CALL);
                }
                ControlFlowInstruction::Unknown { .. } => return Err(reject::CF_UNKNOWN),
            }
        }

        match self.stage {
            Stage::Vertex => {
                self.push("var out: VsOut;");
                self.push("out.position = opos;");
                self.push("out.color = ocolor;");
                self.push("return out;");
            }
            Stage::Pixel => {
                self.push("var out: FsOut;");
                self.push("out.color0 = ocolor0;");
                self.push("return out;");
            }
        }
        self.indent = 0;
        self.push("}");
        Ok(())
    }

    fn emit_exec(
        &mut self,
        parsed: &ParsedShader,
        address: u32,
        count: u32,
        sequence: u32,
        current_alloc: AllocKind,
    ) -> Result<(), &'static str> {
        for i in 0..(count as usize) {
            let triple_idx = address as usize + i;
            let base = triple_idx * 3;
            if base + 2 >= parsed.instructions.len() {
                return Err(reject::INSTR_OOB);
            }
            let words = [
                parsed.instructions[base],
                parsed.instructions[base + 1],
                parsed.instructions[base + 2],
            ];
            // sequence: 2 bits per instruction — bit[0]=fetch(1)/ALU(0),
            // bit[1]=serialize (Xenos `ucode.h:226`).
            let is_fetch = ((sequence >> (i * 2)) & 1) != 0;
            if is_fetch {
                match decode_fetch(words) {
                    FetchInstruction::Vertex(vf) => self.emit_vfetch(&vf)?,
                    FetchInstruction::Texture(tf) => {
                        if tf.dimension != 1 {
                            return Err(reject::TFETCH_NON2D);
                        }
                        self.emit_tfetch(&tf);
                    }
                    FetchInstruction::Unknown { .. } => return Err(reject::VFETCH_FMT),
                }
            } else {
                let alu = decode_alu(words);
                self.emit_alu(&alu, current_alloc)?;
            }
        }
        Ok(())
    }

    fn emit_alu(
        &mut self,
        alu: &AluInstruction,
        current_alloc: AllocKind,
    ) -> Result<(), &'static str> {
        // GPUBUG-100/101: per-operand temp-vs-constant selector (w0
        // bits 29-31), 8-bit component-relative swizzle (w1 bytes 0-2),
        // and 1-bit negate (w1 bits 24-26). Pre-fix all three were
        // discarded, so every ALU read came back as r[low7] without
        // any swizzle / negation, dropping every shader's uniforms +
        // negative operands.
        let a = src_operand(alu.src_a, alu.src_a_is_temp, alu.src_a_swiz, alu.src_a_negate);
        let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate);
        let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate);

        // Vector pipe.
        if alu.vector_write_mask != 0 {
            let expr = vector_expr(alu.vector_opcode, &a, &b, &c)
                .ok_or(reject::VEC_OP_UNSUPPORTED)?;
            let dst_reg = alu.vector_dest & 0x7F;
            if alu.vector_dest_is_export {
                self.emit_export(dst_reg, current_alloc, &expr, alu.vector_write_mask);
            } else {
                self.emit_masked_write(&format!("r[{dst_reg}u]"), &expr, alu.vector_write_mask);
            }
        }

        // Scalar pipe. Binary ops use (src_a.x, src_b.x); ps-variants use
        // src_a.x + running ps. `scl_src_a` mirrors the interpreter's
        // `scalar_src_is_ps` selector.
        let scl_src_a = if alu.scalar_src_is_ps {
            "ps".to_string()
        } else {
            format!("{}.x", a)
        };
        let scl_src_b = format!("{}.x", b);
        let expr = scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps")
            .ok_or(reject::SCL_OP_UNSUPPORTED)?;
        self.push(&format!("ps = {expr};"));
        if alu.scalar_write_mask != 0 {
            let v = "vec4<f32>(ps, ps, ps, ps)";
            let dst_reg = alu.scalar_dest & 0x7F;
            self.emit_masked_write(&format!("r[{dst_reg}u]"), v, alu.scalar_write_mask);
        }
        Ok(())
    }

    fn emit_masked_write(&mut self, lhs: &str, rhs: &str, mask: u8) {
        if mask == 0xF {
            self.push(&format!("{lhs} = {rhs};"));
            return;
        }
        self.push(&"{".to_string());
        self.indent += 1;
        self.push(&format!("let _prev = {lhs};"));
        self.push(&format!("let _new = {rhs};"));
        let mut components = Vec::new();
        let letters = ['x', 'y', 'z', 'w'];
        for (i, c) in letters.iter().enumerate() {
            if (mask >> i) & 1 == 1 {
                components.push(format!("_new.{c}"));
            } else {
                components.push(format!("_prev.{c}"));
            }
        }
        self.push(&format!(
            "{lhs} = vec4<f32>({}, {}, {}, {});",
            components[0], components[1], components[2], components[3]
        ));
        self.indent -= 1;
        self.push("}");
    }

    fn emit_export(&mut self, dst_reg: u8, alloc: AllocKind, expr: &str, mask: u8) {
        // Xenos's export "register" indexing within an alloc range is
        // normally (alloc_base + offset). Since our CF stream doesn't
        // carry per-export slot offsets cleanly, use `alloc` to pick the
        // target.
        let lhs = match (self.stage, alloc) {
            (Stage::Vertex, AllocKind::Position) => "opos",
            (Stage::Vertex, AllocKind::Interpolators) => "ocolor",
            (Stage::Vertex, AllocKind::Colors) => "ocolor",
            (Stage::Vertex, _) => "ocolor", // fall through — any other alloc
            (Stage::Pixel, AllocKind::Colors) => "ocolor0",
            (Stage::Pixel, _) => "ocolor0",
        };
        let _ = dst_reg; // per-slot export indexing reserved for a richer v2
        self.emit_masked_write(lhs, expr, mask);
    }

    fn emit_vfetch(&mut self, vf: &crate::ucode::fetch::VertexFetch) -> Result<(), &'static str> {
        // v1: treat all vertex fetches as R32G32B32A32_FLOAT, stride = 4
        // dwords. Matches the interpreter's MVP semantics; unlocks more
        // formats alongside the CPU texture cache's format expansion.
        //
        // GPUBUG-102: the fetch constant (xe_gpu_vertex_fetch_t,
        // xenos.h:1158-1172) holds the endian field in dword_1's low
        // 2 bits. Vertex data on Xbox 360 is big-endian; the host is
        // little-endian. Pre-fix, every dword was bitcast as-is →
        // vertex positions were byte-reversed garbage and any draw
        // that did reach the host produced clipped / NaN positions.
        let fetch_const = (vf.raw[0] >> 5) & 0x1F;
        let src_reg = vf.src_register & 0x7F;
        let dst_reg = vf.dest_register & 0x7F;
        self.push(&format!(
            "{{ let fc0 = xenos_consts.fetch[{fc0_idx}u]; \
             let fc1 = xenos_consts.fetch[{fc1_idx}u]; \
             let endian = fc1 & 0x3u; \
             let abs_base = (fc0 & 0xFFFFFFFCu) >> 2u; \
             let base = select(abs_base, abs_base - draw_ctx.vertex_base_dwords, \
                 draw_ctx.vertex_base_dwords != 0u && abs_base >= draw_ctx.vertex_base_dwords); \
             let vidx = u32(r[{src_reg}u].x); \
             let addr = base + vidx * 4u; \
             let n = arrayLength(&vertex_buffer); \
             if (addr + 3u < n) {{ \
                 r[{dst_reg}u] = vec4<f32>( \
                     bitcast<f32>(gpu_swap(vertex_buffer[addr + 0u], endian)), \
                     bitcast<f32>(gpu_swap(vertex_buffer[addr + 1u], endian)), \
                     bitcast<f32>(gpu_swap(vertex_buffer[addr + 2u], endian)), \
                     bitcast<f32>(gpu_swap(vertex_buffer[addr + 3u], endian))); \
             }} }}",
            fc0_idx = fetch_const * 2,
            fc1_idx = fetch_const * 2 + 1,
        ));
        Ok(())
    }

    fn emit_tfetch(&mut self, tf: &crate::ucode::fetch::TextureFetch) {
        // v1: sample the single bound texture; UV = r[src].xy. P5's cache
        // publishes the `fetch_const=0` texture into `@group(1)`; slot
        // mismatch is a silent magenta for now.
        let src_reg = tf.src_register & 0x7F;
        let dst_reg = tf.dest_register & 0x7F;
        self.push(&format!(
            "r[{dst_reg}u] = textureSampleLevel(xenos_tex, xenos_samp, r[{src_reg}u].xy, 0.0);"
        ));
    }
}

/// Emit the WGSL expression that reads an ALU source operand with
/// swizzle + negate applied (no abs — see GPUBUG-100 deferred). Mirrors
/// the interpreter shader's `read_src` + `apply_swizzle` + the negate
/// half of `apply_modifiers`. The 8-bit `swizzle` is component-relative
/// per canary `AluInstruction::GetSwizzledComponentIndex`: for output
/// component i, source component is `((swiz >> (2*i)) + i) & 3`.
/// Identity swizzle is `0x00`. GPUBUG-100 / GPUBUG-101.
fn src_operand(src_byte: u8, is_temp: bool, swizzle: u8, negate: bool) -> String {
    let base = if is_temp {
        format!("r[{}u]", (src_byte & 0x3F) as u32)
    } else {
        format!("xenos_consts.alu[{}u]", src_byte as u32)
    };
    let s = swizzle as u32;
    let lane = |i: u32| -> char {
        let c = (((s >> (2 * i)) + i) & 3) as usize;
        ['x', 'y', 'z', 'w'][c]
    };
    // Identity swizzle (0x00) maps to .xyzw — emit a bare expression.
    let swizzled = if swizzle == 0 {
        base
    } else {
        let lx = lane(0);
        let ly = lane(1);
        let lz = lane(2);
        let lw = lane(3);
        format!("vec4<f32>({base}.{lx}, {base}.{ly}, {base}.{lz}, {base}.{lw})")
    };
    if negate {
        format!("(-{swizzled})")
    } else {
        swizzled
    }
}

fn vector_expr(op: u8, a: &str, b: &str, c: &str) -> Option<String> {
    // Semantics mirror the runtime interpreter's `exec_vector_op`
    // (`shaders/xenos_interp.wgsl`), which in turn mirrors canary's
    // `AluVectorOpcode` (ucode.h:1001+). Side-effecting ops (kill*, setp_push)
    // need per-invocation state the AOT emitter doesn't track yet → still
    // `None` (interpreter fallback).
    let cmp4 = |op: &str| {
        format!(
            "vec4<f32>(select(0.0,1.0,{a}.x{op}{b}.x), select(0.0,1.0,{a}.y{op}{b}.y), select(0.0,1.0,{a}.z{op}{b}.z), select(0.0,1.0,{a}.w{op}{b}.w))"
        )
    };
    // CND* : per-lane select(c, b, a <cmp> 0).
    let cnd4 = |op: &str| {
        format!(
            "vec4<f32>(select({c}.x,{b}.x,{a}.x{op}0.0), select({c}.y,{b}.y,{a}.y{op}0.0), select({c}.z,{b}.z,{a}.z{op}0.0), select({c}.w,{b}.w,{a}.w{op}0.0))"
        )
    };
    let s = match op {
        vop::ADD => format!("({a} + {b})"),
        vop::MUL => format!("({a} * {b})"),
        vop::MAX => format!("max({a}, {b})"),
        vop::MIN => format!("min({a}, {b})"),
        vop::MAD => format!("({a} * {b} + {c})"),
        vop::DOT4 => format!("vec4<f32>(dot({a}, {b}))"),
        vop::DOT3 => format!("vec4<f32>(dot({a}.xyz, {b}.xyz))"),
        vop::DOT2_ADD => format!("vec4<f32>({a}.x * {b}.x + {a}.y * {b}.y + {c}.x)"),
        vop::SEQ => cmp4("=="),
        vop::SGT => cmp4(">"),
        vop::SGE => cmp4(">="),
        vop::SNE => cmp4("!="),
        vop::CND_EQ => cnd4("=="),
        vop::CND_GE => cnd4(">="),
        vop::CND_GT => cnd4(">"),
        vop::FRC => format!("fract({a})"),
        vop::TRUNC => format!("trunc({a})"),
        vop::FLOOR => format!("floor({a})"),
        vop::MAX4 => format!("vec4<f32>(max(max({a}.x,{a}.y), max({a}.z,{a}.w)))"),
        // dst = (1, src0.y*src1.y, src0.z, src1.w)  (canary kDst)
        vop::DST => format!("vec4<f32>(1.0, {a}.y * {b}.y, {a}.z, {b}.w)"),
        _ => return None,
    };
    Some(s)
}

fn scalar_expr(op: u8, a: &str, b: &str, prev: &str) -> Option<String> {
    // Semantics mirror the runtime interpreter's `exec_scalar_op`
    // (`shaders/xenos_interp.wgsl`) / canary's `AluScalarOpcode`
    // (ucode.h:1001+). Side-effecting ops (setp*, kills*, maxas*) need
    // per-invocation predicate/kill/address state the AOT emitter doesn't
    // track yet → still `None` (interpreter fallback).
    let s = match op {
        sop::ADDS => format!("({a} + {b})"),
        sop::ADDS_PREV => format!("({a} + {prev})"),
        sop::MULS => format!("({a} * {b})"),
        sop::MULS_PREV => format!("({a} * {prev})"),
        // muls_prev2 / LIT emulation (canary kMulsPrev2): guard against
        // -FLT_MAX / non-finite ps & b, and b <= 0.
        sop::MULS_PREV2 => format!(
            "select({a} * {prev}, -3.4028235e38, {prev} == -3.4028235e38 || !(\
             {prev} == {prev}) || abs({prev}) > 3.4028235e38 || !({b} == {b}) || \
             abs({b}) > 3.4028235e38 || {b} <= 0.0)"
        ),
        sop::MAXS => format!("max({a}, {b})"),
        sop::MINS => format!("min({a}, {b})"),
        sop::SEQS => format!("select(0.0, 1.0, {a} == 0.0)"),
        sop::SGTS => format!("select(0.0, 1.0, {a} > 0.0)"),
        sop::SGES => format!("select(0.0, 1.0, {a} >= 0.0)"),
        sop::SNES => format!("select(0.0, 1.0, {a} != 0.0)"),
        sop::FRCS => format!("fract({a})"),
        sop::TRUNCS => format!("trunc({a})"),
        sop::FLOORS => format!("floor({a})"),
        sop::SUBS => format!("({a} - {b})"),
        sop::SUBS_PREV => format!("({a} - {prev})"),
        sop::EXP => format!("exp2({a})"),
        sop::LOG | sop::LOGC => format!("select(log2({a}), 0.0, {a} == 1.0)"),
        sop::RCP | sop::RCPC | sop::RCPF => format!("xe_rcp({a})"),
        sop::RSQ | sop::RSQC | sop::RSQF => {
            format!("select(0.0, inverseSqrt({a}), {a} > 0.0)")
        }
        sop::SQRT => format!("select(0.0, sqrt({a}), {a} >= 0.0)"),
        sop::SIN => format!("sin({a})"),
        sop::COS => format!("cos({a})"),
        sop::RETAIN_PREV => prev.to_string(),
        _ => return None,
    };
    Some(s)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ucode::alu::{sop, vop};
    use crate::ucode::control_flow::ControlFlowInstruction;

    fn synthetic_trivial_shader() -> ParsedShader {
        // Single Exec clause: ALU add r0 = r0 + r0; scalar_op = RETAIN_PREV
        // with full write-mask on vector, zero on scalar. Alloc(Position)
        // precedes so the ALU's export (if it were one) would target oPos.
        // Word-0 bits 29-31 set so all three operands resolve as temps —
        // matches the prior assertion `r[0u] = (r[0u] + r[0u])`.
        let w0 = (1u32 << 29) | (1u32 << 30) | (1u32 << 31);
        let w2 = (vop::ADD as u32)
            | ((sop::RETAIN_PREV as u32) << 6)
            | (0xF << 12) // vector_write_mask
            | (0u32 << 16); // vector_dest = 0
        ParsedShader {
            cf: vec![
                ControlFlowInstruction::Alloc {
                    size: 1,
                    kind: AllocKind::Position,
                },
                ControlFlowInstruction::Exec {
                    address: 0,
                    count: 1,
                    sequence: 0,
                    is_end: true,
                    predicated: false,
                    predicate_condition: false,
                },
            ],
            instructions: vec![w0, 0, w2],
        }
    }

    #[test]
    fn trivial_shader_translates() {
        let shader = synthetic_trivial_shader();
        match translate(&shader, Stage::Vertex) {
            Translation::Ok(body) => {
                assert!(body.contains("fn vs_main"));
                assert!(body.contains("r[0u] = (r[0u] + r[0u]);"));
                assert!(body.contains("return out;"));
            }
            Translation::Reject(r) => panic!("rejected: {r}"),
        }
    }

    #[test]
    fn combined_module_parses_as_wgsl() {
        let shader = synthetic_trivial_shader();
        let vs = match translate(&shader, Stage::Vertex) {
            Translation::Ok(body) => body,
            Translation::Reject(r) => panic!("VS rejected: {r}"),
        };
        let ps = match translate(&shader, Stage::Pixel) {
            Translation::Ok(body) => body,
            Translation::Reject(r) => panic!("PS rejected: {r}"),
        };
        let module = combine_stages(&vs, &ps);
        // naga is pinned as a dev-dep in this crate; if this fails the
        // translator is emitting invalid WGSL.
        match naga::front::wgsl::parse_str(&module) {
            Ok(_) => {}
            Err(e) => panic!(
                "emitted WGSL failed to parse:\n{}\n--- module ---\n{}",
                e, module
            ),
        }
    }

    #[test]
    fn src_operand_decodes_temp_vs_constant_no_modifiers() {
        // GPUBUG-101: is_temp=true → r[low6]; is_temp=false → xenos_consts.alu[full].
        // Identity swizzle (0x00), no negate → bare base expression.
        assert_eq!(src_operand(0x00, true, 0x00, false), "r[0u]");
        assert_eq!(src_operand(0x05, true, 0x00, false), "r[5u]");
        assert_eq!(src_operand(0x3F, true, 0x00, false), "r[63u]");
        // For temps, bits 6/7 are reserved (abs/rel) — they don't widen
        // the register index even if set. Phase D2 will consume them.
        assert_eq!(src_operand(0x80, true, 0x00, false), "r[0u]");
        assert_eq!(src_operand(0xFF, true, 0x00, false), "r[63u]");
        // Constants: full 8-bit index.
        assert_eq!(src_operand(0x00, false, 0x00, false), "xenos_consts.alu[0u]");
        assert_eq!(src_operand(0x05, false, 0x00, false), "xenos_consts.alu[5u]");
        assert_eq!(src_operand(0xFF, false, 0x00, false), "xenos_consts.alu[255u]");
    }

    #[test]
    fn src_operand_applies_swizzle_and_negate() {
        // GPUBUG-100. Component-relative swizzle. swizzle=0x1B reverses
        // the lanes (.wzyx): for i=0 → ((0x1B >> 0) + 0) & 3 = 3 = w;
        // for i=1 → ((0x1B >> 2) + 1) & 3 = (6+1)&3 = 3 = w. Hmm —
        // canary's identity is 0x00 = .xyzw, so .wzyx in component-
        // relative terms = `s0=3, s1=2, s2=1, s3=0` → bits would be
        // (3, (2-1)&3=1, (1-2)&3=3, (0-3)&3=1) which combines weirdly.
        // We just verify the mechanics by precomputing a known case:
        // swizzle=0x00 (identity) outputs .xyzw — matched by no-swizzle
        // branch. Negate wraps in `(-…)`.
        assert_eq!(src_operand(0x05, true, 0x00, true), "(-r[5u])");
        // swizzle=0xFF → for each i, ((0xFF >> (2i)) + i) & 3:
        //   i=0: (3 + 0) & 3 = 3 → w
        //   i=1: ((0x3F) + 1) & 3 = (63+1)&3 = 0 → x
        //   i=2: ((0x0F) + 2) & 3 = (15+2)&3 = 1 → y
        //   i=3: ((0x03) + 3) & 3 = (3+3)&3 = 2 → z
        // Output: .wxyz
        assert_eq!(
            src_operand(0x05, true, 0xFF, false),
            "vec4<f32>(r[5u].w, r[5u].x, r[5u].y, r[5u].z)"
        );
        // Combined: negate of constant with .wxyz swizzle.
        assert_eq!(
            src_operand(0x07, false, 0xFF, true),
            "(-vec4<f32>(xenos_consts.alu[7u].w, xenos_consts.alu[7u].x, xenos_consts.alu[7u].y, xenos_consts.alu[7u].z))"
        );
    }

    #[test]
    fn shader_using_c0_emits_xenos_consts_read() {
        // ALU: r0 = c0 + r0. src_a (low byte) is constant index 0;
        // src_b (next byte) is temp index 0. src_a_is_temp=false →
        // src1_sel-style bit at w0 bit 29 = 0; src_b_is_temp=true →
        // bit 30 = 1. (src_c left as 0/temp; unused.)
        let w0 = 0x00u32 // src_a = c0
            | (0x00u32 << 8) // src_b = r0
            | (0x00u32 << 16) // src_c
            | (0u32 << 29) // src_a_is_temp = false (constant)
            | (1u32 << 30); // src_b_is_temp = true (register)
        let w2 = (vop::ADD as u32)
            | ((sop::RETAIN_PREV as u32) << 6)
            | (0xF << 12)
            | (0u32 << 16);
        let shader = ParsedShader {
            cf: vec![
                ControlFlowInstruction::Alloc {
                    size: 1,
                    kind: AllocKind::Position,
                },
                ControlFlowInstruction::Exec {
                    address: 0,
                    count: 1,
                    sequence: 0,
                    is_end: true,
                    predicated: false,
                    predicate_condition: false,
                },
            ],
            instructions: vec![w0, 0, w2],
        };
        match translate(&shader, Stage::Vertex) {
            Translation::Ok(body) => {
                assert!(
                    body.contains("xenos_consts.alu[0u]"),
                    "expected c0 operand, got: {body}"
                );
                assert!(
                    body.contains("r[0u]"),
                    "expected r0 temp operand, got: {body}"
                );
            }
            Translation::Reject(r) => panic!("rejected: {r}"),
        }
    }

    #[test]
    fn vfetch_emit_includes_gpu_swap_helper_call() {
        // GPUBUG-102: emit_vfetch should reference `gpu_swap(...)` for
        // each lane. Ensures the per-format endian byte-swap is wired
        // into the AOT path.
        let mut ctx = EmitCtx::new(Stage::Vertex);
        let vf = crate::ucode::fetch::VertexFetch {
            fetch_const: 0,
            src_register: 0,
            dest_register: 0,
            dest_write_mask: 0xF,
            raw: [0; 3],
        };
        ctx.emit_vfetch(&vf).expect("emit_vfetch");
        let body = ctx.finish();
        assert!(body.contains("gpu_swap("), "emitted vfetch body: {body}");
    }

    #[test]
    fn loop_clause_rejected() {
        let shader = ParsedShader {
            cf: vec![ControlFlowInstruction::LoopStart {
                address: 0,
                loop_id: 0,
            }],
            instructions: vec![],
        };
        assert!(matches!(
            translate(&shader, Stage::Vertex),
            Translation::Reject(reject::CF_LOOP)
        ));
    }

    #[test]
    fn unsupported_op_rejected() {
        let w2 = (29u32) // VOP_MAX_A, not in v1 subset
            | ((sop::RETAIN_PREV as u32) << 6)
            | (0xF << 12);
        let shader = ParsedShader {
            cf: vec![ControlFlowInstruction::Exec {
                address: 0,
                count: 1,
                sequence: 0,
                is_end: true,
                predicated: false,
                predicate_condition: false,
            }],
            instructions: vec![0, 0, w2],
        };
        assert!(matches!(
            translate(&shader, Stage::Vertex),
            Translation::Reject(reject::VEC_OP_UNSUPPORTED)
        ));
    }
}
