//! Per-draw geometry capture for the host UI's faithful-render path.
//!
//! The deterministic headless core (`check --gpu-inline`) never touches this
//! module — it is populated only when a UI bridge is installed and consumed
//! only by `crates/xenia-ui`. The goal is to hand the UI the *real* guest
//! geometry behind each `PM4_DRAW_INDX*` packet so it can rasterize the
//! actual splash vertices instead of synthetic placeholder shapes.
//!
//! What the WGSL pipeline needs to reconstruct one draw (see
//! `shaders/xenos_interp.wgsl` `vs_main` / `interpret_vertex_fetch`):
//!   * the active VS/PS blob keys (already published as assets),
//!   * the primitive type + the host vertex count to issue,
//!   * the raw guest vertex-buffer bytes for the fetched window, and
//!   * the *dword base* of that window so the shader can rebase the absolute
//!     fetch-constant address into the uploaded buffer.
//!
//! The hard part is sourcing the vertex window: the VS reads a vertex-fetch
//! constant (`xe_gpu_vertex_fetch_t`) whose dword-0 carries the absolute
//! guest dword address. We parse the active VS, find its first vertex fetch,
//! read that fetch constant out of the register file, then copy a bounded
//! window of guest memory starting at the fetch base.

use xenia_memory::access::MemoryAccess;

use crate::draw_state::{IndexSize, IndexSource, PrimitiveType};
use crate::register_file::RegisterFile;

/// Texture-fetch / vertex-fetch constant region base, in register indices.
/// Each fetch constant is 6 dwords (`xe_gpu_*_fetch_t`).
const CONST_BASE_FETCH: u32 = 0x4800;

/// Upper bound (in dwords) on the vertex window we copy per draw. The splash
/// UI draws are tiny (3–4 verts × ≤4 dwords); 64 KiB of dwords is generous
/// slack while bounding the per-frame copy cost and the 16 MiB host buffer.
const MAX_WINDOW_DWORDS: u32 = 16 * 1024;

/// One captured draw, with enough real state for the UI to replay it through
/// the existing wgpu Xenos pipeline.
#[derive(Clone, Debug)]
pub struct DrawCapture {
    /// Monotonic global draw index (matches `GpuStats::draws_seen` at capture).
    pub draw_index: u32,
    /// Xenos primitive-type code (see `SwapInfo::last_draw_prim` encoding).
    pub prim_code: u32,
    /// Host vertex count to issue (post primitive-processor rewrite).
    pub host_vertex_count: u32,
    /// Active VS blob key at draw time (0 = none).
    pub vs_key: u32,
    /// Active PS blob key at draw time (0 = none).
    pub ps_key: u32,
    /// Raw guest dwords of the fetched vertex window (host-endian as stored in
    /// guest memory — the WGSL applies the per-format endian swap). `addr 0`
    /// of this buffer corresponds to guest dword `window_base_dwords`.
    pub vertex_dwords: Vec<u32>,
    /// Guest dword address that maps to index 0 of `vertex_dwords`. The shader
    /// subtracts this from the fetch-constant base to index `vertex_dwords`.
    pub window_base_dwords: u32,
    /// `true` when we successfully resolved a real vertex window. When `false`
    /// the UI falls back to its procedural geometry for this draw (honest:
    /// nothing faked, just "couldn't source real vertices").
    pub has_real_vertices: bool,
    /// iterate-3S: per-draw NDC transform derived from the guest viewport /
    /// clip / VTE registers (mirrors canary `GetHostViewportInfo`). The host VS
    /// converts the guest-VS position to wgpu clip space via
    /// `clip.xy = pos.xy * ndc_scale + ndc_offset * pos.w`. The Y component
    /// already carries the render-target → wgpu Y-flip (negated).
    pub ndc_scale: [f32; 2],
    pub ndc_offset: [f32; 2],
    /// iterate-3T: the decoded texture(s) this draw's active pixel shader
    /// samples, keyed off its real `tfetch` fetch-constant slots (the 3M
    /// decoder makes these decode). Root-#3: the UI uploads + binds EACH entry
    /// to its own texture slot per-draw, so a multi-plane shader (e.g. the
    /// intro video's YUV Y/U/V planes) samples the right texture per fetch.
    /// Empty for flat (no-tfetch) draws. Populated by `gpu_system` after
    /// decode (left empty by `build`).
    ///
    /// Each entry is `(slot, key, content_version, bytes)` where `slot` is the
    /// `tfetch` fetch-constant index (0..31) the shader samples this texture
    /// from. iterate-3AD: the `content_version` (from `span_max_version` over
    /// the texel span) lets the UI host texture cache RE-UPLOAD when the guest
    /// fills more of an evolving atlas. The publisher and the 2nd splash logo
    /// share one K8888 surface (base `0x4dbee000`); the 2nd logo's texels are
    /// CPU-written *after* the publisher's first upload. Without the real
    /// version the host cache (which previously pinned `version_when_uploaded
    /// = 1`) kept the first partial upload, so the 2nd logo sampled its
    /// still-zero atlas region as black.
    pub textures: Vec<(u8, crate::texture_cache::TextureKey, u64, Vec<u8>)>,
    /// iterate-3Y: per-draw color/blend render state captured from the
    /// register file so the host pipeline composites the way the guest
    /// intends (instead of one fixed alpha-blend state). Mirrors the fields
    /// canary feeds into `GetCurrentStateDescription` (D3D12
    /// `pipeline_cache.cc`):
    ///   * `blend_control` = `RB_BLENDCONTROL0` (RT0 src/dst factors + op,
    ///     color and alpha). The Xbox 360 has no separate "blend enable" bit;
    ///     `One,Zero,Add` *is* the opaque case.
    ///   * `color_mask` = RT0 nibble of `RB_COLOR_MASK` (per-channel write
    ///     enable). When 0, canary forces `One,Zero` (no blend).
    ///   * `color_control` = `RB_COLORCONTROL` (alpha-test enable/func).
    ///   * `depth_control` = `RB_DEPTHCONTROL` (z-test enable/func/write).
    pub blend_control: u32,
    pub color_mask: u8,
    pub color_control: u32,
    pub depth_control: u32,
}

/// iterate-3S: compute the guest→host NDC XY transform for a draw, mirroring
/// canary's `draw_util.cc::GetHostViewportInfo` (the XY half). The Xbox 360 VS
/// emits a clip-space position which the HW then scales/offsets by the viewport
/// (`PA_CL_VPORT_*`, gated by `PA_CL_VTE_CNTL`) into render-target pixels, OR,
/// when clipping is disabled (`PA_CL_CLIP_CNTL.clip_disable`), the VS emits
/// render-target-pixel coordinates directly (the screen-space UI / clear case —
/// this is what Sylpheed's splash quads do). Either way we must rescale into the
/// host's [-1,1] clip space and flip Y (render-target Y-down → wgpu Y-up).
///
/// Returns `(ndc_scale[2], ndc_offset[2])` such that
/// `host_clip.xy = guest_pos.xy * ndc_scale + ndc_offset * guest_pos.w`.
/// The Y entries are pre-negated to flip into wgpu's Y-up clip space.
pub fn compute_ndc_xy(rf: &RegisterFile) -> ([f32; 2], [f32; 2]) {
    const PA_CL_CLIP_CNTL: u32 = 0x2204;
    const PA_SU_SC_MODE_CNTL: u32 = 0x2205;
    const PA_CL_VTE_CNTL: u32 = 0x2206;
    const PA_SU_VTX_CNTL: u32 = 0x2302;
    const PA_CL_VPORT_XSCALE: u32 = 0x210F;
    const PA_CL_VPORT_XOFFSET: u32 = 0x2110;
    const PA_CL_VPORT_YSCALE: u32 = 0x2111;
    const PA_CL_VPORT_YOFFSET: u32 = 0x2112;
    const PA_SC_WINDOW_OFFSET: u32 = 0x2080;
    const PA_SC_WINDOW_SCISSOR_BR: u32 = 0x2082;
    const RB_SURFACE_INFO: u32 = 0x2000;

    let clip_cntl = rf.read(PA_CL_CLIP_CNTL);
    let vte = rf.read(PA_CL_VTE_CNTL);
    let su_sc_mode = rf.read(PA_SU_SC_MODE_CNTL);
    let su_vtx = rf.read(PA_SU_VTX_CNTL);
    let fbits = |r: u32| f32::from_bits(rf.read(r));

    // VTE enable bits (xenos.h PA_CL_VTE_CNTL): bit0 vport_x_scale_ena,
    // bit1 vport_x_offset_ena, bit2 vport_y_scale_ena, bit3 vport_y_offset_ena.
    let scale_x = if vte & (1 << 0) != 0 { fbits(PA_CL_VPORT_XSCALE) } else { 1.0 };
    let off_x = if vte & (1 << 1) != 0 { fbits(PA_CL_VPORT_XOFFSET) } else { 0.0 };
    let scale_y = if vte & (1 << 2) != 0 { fbits(PA_CL_VPORT_YSCALE) } else { 1.0 };
    let off_y = if vte & (1 << 3) != 0 { fbits(PA_CL_VPORT_YOFFSET) } else { 0.0 };

    // Render-target extent in guest pixels: clamp to the texture max (2048),
    // sourced from the window scissor BR (matches canary `x_max`/`y_max`).
    let br = rf.read(PA_SC_WINDOW_SCISSOR_BR);
    let x_max = ((br & 0x7FFF).max(1)).min(2048) as f32;
    let y_max = (((br >> 16) & 0x7FFF).max(1)).min(2048) as f32;
    let _ = RB_SURFACE_INFO;

    // Half-pixel + window offsets added in render-target pixels.
    let mut add_x = 0.0f32;
    let mut add_y = 0.0f32;
    if su_sc_mode & (1 << 16) != 0 {
        let wo = rf.read(PA_SC_WINDOW_OFFSET);
        // 15-bit signed each (x: [14:0], y: [30:16]).
        let sx = (((wo & 0x7FFF) << 1) as i32) >> 1;
        let sy = ((((wo >> 16) & 0x7FFF) << 1) as i32) >> 1;
        add_x += sx as f32;
        add_y += sy as f32;
    }
    if su_vtx & 1 == 0 {
        // pix_center == kD3DZero → +0.5 half-pixel offset.
        add_x += 0.5;
        add_y += 0.5;
    }

    let (s, o);
    if clip_cntl & (1 << 16) != 0 {
        // clip_disable: VS outputs render-target-*pixel* coords (Y-DOWN: pixel
        // y=0 is the top row of the render target). Rescale the whole RT extent
        // to [-1,1] and FLIP Y so pixel-top → wgpu clip-top (canary's
        // huge-host-viewport path; the framebuffer→clip flip is real here).
        let px2ndc_x = 2.0 / x_max;
        let px2ndc_y = 2.0 / y_max;
        let sx = scale_x * px2ndc_x;
        let ox = (off_x - x_max * 0.5 + add_x) * px2ndc_x;
        let sy = scale_y * px2ndc_y;
        let oy = (off_y - y_max * 0.5 + add_y) * px2ndc_y;
        // Flip Y: pixel-Y-down → wgpu clip-Y-up.
        s = [sx, -sy];
        o = [ox, -oy];
    } else {
        // iterate-3AA (DEFECT 1 ROOT): clipping enabled → the VS already emits
        // *clip-space* coordinates (Y-UP: +Y is the top of the screen), exactly
        // the convention the Xbox 360's D3D9 and wgpu BOTH use for clip space
        // (NDC +Y → framebuffer top in each API; the framebuffer Y-direction is
        // an internal viewport detail handled identically by both). A clip-space
        // position is therefore portable to wgpu with NO Y-flip. The previous
        // code unconditionally negated Y (the same flip the screen-space pixel
        // path needs), which mirrored the publisher logo vertically: its quad is
        // centered (±0.085 around 0) so the *position* stayed centered, but the
        // negation swapped top↔bottom vertices while the texture V was unchanged
        // → the sampled sub-rect (UV v 0.001→0.090) read bottom-up → "SQUARE
        // ENIX" rendered upside down in place. Measured (readback): the red dots
        // sit at 43% from the texture top but rendered at 58% from the top
        // (= a clean vertical mirror); removing the flip restores them to 43%.
        // Identity XY (no flip) maps guest clip-Y-up straight to wgpu clip-Y-up.
        s = [1.0, 1.0];
        o = [0.0, 0.0];
        return (s, o);
    }
    (s, o)
}

/// Encode a [`PrimitiveType`] as the raw Xenos code used across the bridge.
pub fn prim_code(p: PrimitiveType) -> u32 {
    match p {
        PrimitiveType::None => 0,
        PrimitiveType::PointList => 1,
        PrimitiveType::LineList => 2,
        PrimitiveType::LineStrip => 3,
        PrimitiveType::TriangleList => 4,
        PrimitiveType::TriangleFan => 5,
        PrimitiveType::TriangleStrip => 6,
        PrimitiveType::RectangleList => 8,
        PrimitiveType::QuadList => 13,
        PrimitiveType::Unknown(x) => x as u32,
    }
}

/// Resolve the first vertex-fetch window referenced by the parsed VS.
///
/// Walks the VS instruction stream for the first `vfetch` (mini) instruction,
/// reads its fetch constant from `rf`, and copies a bounded window of guest
/// memory starting at the fetch base. Returns `(dwords, window_base_dwords)`
/// or `None` if the VS has no vertex fetch or the constant is malformed.
fn resolve_vertex_window(
    parsed_vs: &crate::ucode::ParsedShader,
    rf: &RegisterFile,
    mem: &dyn MemoryAccess,
) -> Option<(Vec<u32>, u32)> {
    // iterate-3W (GPUBUG-109): the instruction block packs ALU and fetch
    // instructions identically (96 bits / 3 dwords each); ONLY the owning
    // `Exec` control-flow clause's `sequence` bitmap (2 bits per instruction,
    // bit[2*i]=fetch/ALU) tells them apart. The previous blind triple-walk
    // decoded ALU triples as fetches → garbage fetch-constant indices and a
    // bogus `type==3` guard, never reaching the real vertex fetch. Walk the CF
    // exec clauses exactly as the translator does (`translator.rs::emit_exec`)
    // and take the FIRST sequence-flagged *vertex* fetch.
    let instrs = &parsed_vs.instructions;
    let mut const_off: Option<u32> = None;
    'clauses: for clause in &parsed_vs.cf {
        let crate::ucode::control_flow::ControlFlowInstruction::Exec {
            address,
            count,
            sequence,
            ..
        } = *clause
        else {
            continue;
        };
        for i in 0..(count as usize) {
            // bit[2*i] of the sequence bitmap: 1 = fetch, 0 = ALU.
            if (sequence >> (i * 2)) & 1 == 0 {
                continue;
            }
            let base = (address as usize + i) * 3;
            if base + 2 >= instrs.len() {
                break;
            }
            if let crate::ucode::fetch::FetchInstruction::Vertex(vf) =
                crate::ucode::fetch::decode_fetch([instrs[base], instrs[base + 1], instrs[base + 2]])
            {
                const_off = Some(vf.const_reg_offset());
                break 'clauses;
            }
        }
    }
    // iterate-3X (GPUBUG-110): vertex fetch constants are addressed by
    // `const_index * 3 + const_index_sel` (canary `ucode.h:700` —
    // `VertexFetchInstruction::fetch_constant_index`), NOT by `const_index`
    // alone. The register region packs 3 two-dword vertex-fetch constants per
    // 6-dword group, so the constant lives at
    // `0x4800 + const_index*6 + const_index_sel*2`. The previous decode dropped
    // `const_index_sel` and read sub-slot 0 (`fc*6`), which for the publisher
    // logo (`const_index=31, sel=2`) held `0x00000001` (an unused slot) instead
    // of the real vertex-buffer base at sub-slot 2 (`0x48BE`). That made
    // `has_real_vertices=false` → the logo fell to the procedural fullscreen
    // magenta fallback. (Refutes iterate-3W's "geometry is auto-generated from
    // vertex_id" — measured: the real fetch constant is a 4-vertex QuadList
    // buffer at `0x0adf60f0`.)
    let const_reg = CONST_BASE_FETCH + const_off?;
    let dword0 = rf.read(const_reg);
    let dword1 = rf.read(const_reg + 1);
    // address:30 at bits[31:2] of dword0 (in bytes once masked). The fetch
    // constant carries a guest *physical* dword address — canary reads the
    // vertex buffer via `Memory::TranslatePhysical(fetch.address * 4)`
    // (`draw_util.cc:961`). On the Xbox 360 the physical range is mirrored at
    // several virtual windows; ours only maps the cached-physical window at
    // `0x4000_0000` (`gpu_system::physical_to_backing`). Reading the bare low
    // address (`0x0adf_xxxx`) hits an unmapped VA and returns zeros, so rebase
    // a low physical base onto the mapped `0x4000_0000` alias when the raw VA
    // is not itself mapped. `window_base_dwords` keeps the *original* base so
    // the shader's rebase against the (unmodified) fetch-constant address still
    // indexes the uploaded window correctly.
    let base_bytes = dword0 & 0xFFFF_FFFC;
    if base_bytes == 0 {
        return None;
    }
    let read_base = if mem.translate(base_bytes).is_some() {
        base_bytes
    } else if base_bytes < 0x2000_0000 && mem.translate(base_bytes | 0x4000_0000).is_some() {
        base_bytes | 0x4000_0000
    } else {
        base_bytes
    };
    // size:24 at bits[25:2] of dword1, in dwords. Clamp to our window cap.
    let size_dwords = ((dword1 >> 2) & 0x00FF_FFFF).clamp(1, MAX_WINDOW_DWORDS);
    let window_base_dwords = base_bytes >> 2;
    let mut dwords = Vec::with_capacity(size_dwords as usize);
    for i in 0..size_dwords {
        let addr = read_base.wrapping_add(i * 4);
        if addr < read_base {
            break; // wrap guard
        }
        // `read_u32` composes big-endian bytes into the u32 value; the WGSL's
        // `gpu_swap` expects the *raw little-endian dword* as it sits in guest
        // memory, so undo the BE composition with `swap_bytes`.
        dwords.push(mem.read_u32(addr).swap_bytes());
    }
    if dwords.is_empty() {
        return None;
    }
    Some((dwords, window_base_dwords))
}

/// Build a [`DrawCapture`] for one draw. Best-effort: when the vertex window
/// can't be resolved, `has_real_vertices` is `false` and the UI falls back to
/// procedural geometry (never fabricated pixels).
#[allow(clippy::too_many_arguments)]
pub fn build(
    draw_index: u32,
    primitive: PrimitiveType,
    host_vertex_count: u32,
    _index_source: IndexSource,
    _index_size: IndexSize,
    vs_key: u32,
    ps_key: u32,
    parsed_vs: Option<&crate::ucode::ParsedShader>,
    rf: &RegisterFile,
    mem: &dyn MemoryAccess,
) -> DrawCapture {
    let (vertex_dwords, window_base_dwords, has_real) = match parsed_vs
        .and_then(|vs| resolve_vertex_window(vs, rf, mem))
    {
        Some((d, base)) => (d, base, true),
        None => (Vec::new(), 0, false),
    };
    let (ndc_scale, ndc_offset) = compute_ndc_xy(rf);
    // iterate-3Y: capture RT0 color/blend/depth render state. Registers per
    // canary `registers.h`: RB_BLENDCONTROL0=0x2201, RB_COLOR_MASK=0x2104
    // (RT0 = bits[3:0]), RB_COLORCONTROL=0x2202, RB_DEPTHCONTROL=0x2200.
    const RB_BLENDCONTROL_0: u32 = 0x2201;
    const RB_COLOR_MASK: u32 = 0x2104;
    const RB_COLORCONTROL: u32 = 0x2202;
    const RB_DEPTHCONTROL: u32 = 0x2200;
    DrawCapture {
        draw_index,
        prim_code: prim_code(primitive),
        host_vertex_count,
        vs_key,
        ps_key,
        vertex_dwords,
        window_base_dwords,
        has_real_vertices: has_real,
        ndc_scale,
        ndc_offset,
        textures: Vec::new(),
        blend_control: rf.read(RB_BLENDCONTROL_0),
        color_mask: (rf.read(RB_COLOR_MASK) & 0xF) as u8,
        color_control: rf.read(RB_COLORCONTROL),
        depth_control: rf.read(RB_DEPTHCONTROL),
    }
}
