//! Render frame management.
//!
//! `RenderFrame` is responsible for:
//! - Holding extracted scene data ([`ExtractedScene`]) and render state ([`RenderState`])
//! - Running the Extract and Prepare phases of the frame pipeline
//!
//! # Three-Phase Rendering Architecture
//!
//! The rendering pipeline is divided into three distinct phases:
//!
//! 1. **Prepare**: `extract_and_prepare()` — extract scene data and prepare GPU resources
//! 2. **Compose**: Chain render nodes via [`FrameComposer`]
//! 3. **Execute**: `FrameComposer::render()` — acquire the surface and submit GPU commands
//!

use glam::Mat4;
use rustc_hash::FxHashMap;

use crate::core::{BindGroupContext, RenderView, ResourceManager};
use crate::pipeline::RenderPipelineId;
use crate::renderer::FrameTime;
use myth_assets::{AssetServer, GeometryHandle, MaterialHandle};
use myth_scene::Scene;
use myth_scene::camera::RenderCamera;

use super::extracted::ExtractedScene;
use super::render_state::RenderState;
use super::shadow_utils;

// ============================================================================
// RenderCommand & RenderLists
// ============================================================================

/// A single render command.
///
/// Contains all information needed to draw one object. Produced by `CullPass`,
/// consumed by `OpaquePass` / `TransparentPass`.
///
/// # Performance Notes
/// - Pipeline is obtained via `clone` (`wgpu::RenderPipeline` is internally `Arc`)
/// - `dynamic_offset` supports dynamic uniform buffering
/// - `sort_key` enables efficient sorting (front-to-back / back-to-front)
pub struct RenderCommand {
    /// Per-object bind group (model matrix, skeleton, etc.)
    pub object_bind_group: BindGroupContext,
    /// Geometry handle
    pub geometry_handle: GeometryHandle,
    /// Material handle
    pub material_handle: MaterialHandle,
    /// Pipeline handle (index into [`PipelineCache`] storage).
    ///
    /// Resolve to a `&wgpu::RenderPipeline` via
    /// [`PipelineCache::get_render_pipeline`] during the execute phase.
    pub pipeline_id: RenderPipelineId,
    /// Sort key
    pub sort_key: RenderKey,
    /// Dynamic uniform offset
    pub dynamic_offset: u32,
}

pub struct ShadowRenderCommand {
    pub object_bind_group: BindGroupContext,
    pub geometry_handle: GeometryHandle,
    pub material_handle: MaterialHandle,
    /// Pipeline handle (index into [`PipelineCache`] storage).
    pub pipeline_id: RenderPipelineId,
    pub dynamic_offset: u32,
}

// ============================================================================
// DrawCommand — Baked Physical Draw Command
// ============================================================================

/// A single pre-baked draw command with all GPU references resolved to physical
/// `wgpu` handles.
///
/// Produced by [`bake::bake_render_lists`] after culling, consumed by the
/// unified [`submit_draw_commands`] helper during the execute phase.  Contains
/// **zero** asset handles — only physical references — so the execute phase
/// never performs hash-map lookups or indirection.
///
/// # Design Philosophy — "The Blind Execute Phase"
///
/// All semantic decisions (pipeline variant selection, alpha-test detection,
/// material/geometry resolution) are completed during the earlier **cull /
/// bake** phase.  The execute phase sees only hardware-level GPU state and
/// can be trivially parallelised in the future.
///
/// [`bake::bake_render_lists`]: super::bake::bake_render_lists
/// [`submit_draw_commands`]: super::rdg::draw::submit_draw_commands
pub struct DrawCommand<'a> {
    /// Composite sort key for minimising GPU state switches.
    ///
    /// * Opaque — Pipeline › Material › Depth (front-to-back).
    /// * Transparent — Depth (back-to-front) › Pipeline › Material.
    /// * Shadow — Pipeline (state-switch minimisation).
    pub sort_key: u64,

    /// Pre-resolved render pipeline.
    ///
    /// Redundant-state elimination is performed by comparing the raw
    /// pointer address — `wgpu::RenderPipeline` is `Arc`-based so
    /// identity ≡ pointer equality.
    pub pipeline: &'a wgpu::RenderPipeline,

    /// Pre-resolved vertex buffer bindings.
    ///
    /// Typically 1–2 entries.  Uses `Vec` (covariant in `'a`) to maintain
    /// correct lifetime variance for the execute-phase borrow chain.
    pub vertex_buffers: Vec<&'a wgpu::Buffer>,

    /// Pre-resolved index buffer, or `None` for non-indexed draws.
    ///
    /// Tuple: `(buffer, format, index_count)`.
    pub index_buffer: Option<(&'a wgpu::Buffer, wgpu::IndexFormat, u32)>,

    /// Material bind group (Group 1), or `None` when unused.
    pub bind_group_1: Option<&'a wgpu::BindGroup>,

    /// Object / transform bind group (Group 2) with dynamic uniform offset.
    ///
    /// Tuple: `(bind_group, dynamic_offset)`.
    pub bind_group_2: (&'a wgpu::BindGroup, u32),

    /// Screen / transient bind group (Group 3), or `None` for shadow/prepass.
    pub bind_group_3: Option<&'a wgpu::BindGroup>,

    /// Stencil reference value for feature-ID writing.  `None` when unused.
    pub stencil_reference: Option<u32>,

    /// Vertex range for non-indexed draws.
    pub vertex_range: std::ops::Range<u32>,

    /// Instance range.
    pub instance_range: std::ops::Range<u32>,
}

/// Frame-scoped pre-baked render command lists.
///
/// All GPU handle lookups have been resolved to physical `wgpu` references.
/// The execute phase iterates these contiguous `Vec`s without dictionary
/// lookups, maximising CPU cache hit rate.
pub struct BakedRenderLists<'a> {
    /// Baked opaque draw commands (sorted front-to-back).
    pub opaque: Vec<DrawCommand<'a>>,

    /// Baked transparent draw commands (sorted back-to-front).
    pub transparent: Vec<DrawCommand<'a>>,

    /// Baked Z-prepass draw commands (prepass-specific pipelines).
    pub prepass: Vec<DrawCommand<'a>>,

    /// Per-shadow-view baked draw commands, keyed by
    /// `(light_id, layer_index)`.
    pub shadow_queues: FxHashMap<(u64, u32), Vec<DrawCommand<'a>>>,

    pub global_bind_group: &'a wgpu::BindGroup,
}

#[derive(Clone, Copy)]
pub struct ShadowLightInstance {
    pub light_id: u64,
    pub layer_index: u32,
    pub light_buffer_index: usize,
    pub light_view_projection: Mat4,
}

/// Prepared skybox draw state for inline rendering (LDR path).
///
/// Populated by [`SkyboxPass::prepare()`] and consumed by
/// [`SimpleForwardPass::run()`] to draw the skybox between
/// opaque and transparent objects within a single render pass.
pub struct PreparedSkyboxDraw<'a> {
    /// Pre-resolved skybox render pipeline reference.
    pub pipeline: &'a wgpu::RenderPipeline,
    /// The skybox bind group (uniforms + optional texture/sampler).
    pub bind_group: &'a wgpu::BindGroup,
}

impl<'a> PreparedSkyboxDraw<'a> {
    pub fn draw(&self, pass: &mut wgpu::RenderPass<'a>, global_bind_group: &'a wgpu::BindGroup) {
        pass.set_pipeline(self.pipeline);
        pass.set_bind_group(0, global_bind_group, &[]);
        pass.set_bind_group(1, self.bind_group, &[]);
        pass.draw(0..3, 0..1);
    }
}

/// Render lists.
///
/// Stores culled and sorted render commands. Populated by `SceneCullPass`,
/// consumed by `OpaquePass`, `TransparentPass`, and `SimpleForwardPass`.
///
/// # Design Principles
/// - **Data-logic separation**: stores data only, contains no rendering logic
/// - **Inter-frame reuse**: pre-allocated memory, cleared via `clear()` each frame
/// - **Extensible**: can add `alpha_test`, `shadow_casters`, etc. in the future
pub struct RenderLists {
    /// Opaque command list (front-to-back sorted)
    pub opaque: Vec<RenderCommand>,
    /// Transparent command list (back-to-front sorted)
    pub transparent: Vec<RenderCommand>,
    /// Shadow command queues, keyed by `(light_id, layer_index)` for per-view culling.
    ///
    /// Each cascade of a directional light (or each spot light) gets its own queue.
    pub shadow_queues: FxHashMap<(u64, u32), Vec<ShadowRenderCommand>>,
    pub shadow_lights: Vec<ShadowLightInstance>,

    /// All active render views for the current frame.
    ///
    /// Populated by `SceneCullPass`, consumed by `ShadowPass` and other passes.
    /// Contains main camera view + all shadow views.
    pub active_views: Vec<RenderView>,

    /// Global bind group (camera, lighting, environment, etc.)
    pub gpu_global_bind_group: Option<wgpu::BindGroup>,

    /// Whether a transmission copy is needed this frame
    pub use_transmission: bool,
}

impl RenderLists {
    /// Creates empty render lists with pre-allocated default capacity.
    #[must_use]
    pub fn new() -> Self {
        Self {
            opaque: Vec::with_capacity(512),
            transparent: Vec::with_capacity(128),
            shadow_queues: FxHashMap::default(),
            shadow_lights: Vec::with_capacity(16),
            active_views: Vec::with_capacity(16),
            gpu_global_bind_group: None,
            use_transmission: false,
        }
    }

    /// Clears all lists (retains capacity for memory reuse).
    #[inline]
    pub fn clear(&mut self) {
        self.opaque.clear();
        self.transparent.clear();
        self.shadow_queues.clear();
        self.shadow_lights.clear();
        self.active_views.clear();
        self.gpu_global_bind_group = None;
        self.use_transmission = false;
    }

    /// Inserts an opaque render command.
    #[inline]
    pub fn insert_opaque(&mut self, cmd: RenderCommand) {
        self.opaque.push(cmd);
    }

    /// Inserts a transparent render command.
    #[inline]
    pub fn insert_transparent(&mut self, cmd: RenderCommand) {
        self.transparent.push(cmd);
    }

    /// Sorts command lists.
    ///
    /// - Opaque: by Pipeline > Material > Depth (front-to-back)
    /// - Transparent: by Depth (back-to-front) > Pipeline > Material
    pub fn sort(&mut self) {
        self.opaque
            .sort_unstable_by(|a, b| a.sort_key.cmp(&b.sort_key));
        self.transparent
            .sort_unstable_by(|a, b| a.sort_key.cmp(&b.sort_key));
    }

    /// Returns `true` if both lists are empty.
    #[inline]
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.opaque.is_empty() && self.transparent.is_empty()
    }
}

impl Default for RenderLists {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// RenderKey — Sort Key
// ============================================================================

/// Render sort key (Pipeline ID + Material ID + Depth).
///
/// Encodes sorting information in a 64-bit integer for efficient radix sorting.
///
/// # Sorting Strategy
/// - **Opaque objects**: Pipeline > Material > Depth (front-to-back)
///   - Minimizes pipeline state switches
///   - Front-to-back leverages Early-Z for performance
/// - **Transparent objects**: Depth (back-to-front) > Pipeline > Material
///   - Ensures correct alpha blending order
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RenderKey(u64);

impl RenderKey {
    /// Returns the raw 64-bit sort key value.
    #[inline]
    #[must_use]
    pub fn bits(self) -> u64 {
        self.0
    }

    /// Constructs a sort key.
    ///
    /// # Parameters
    /// - `pipeline_id`: Pipeline handle
    /// - `material_index`: Material index (20 bits)
    /// - `depth`: Squared distance to the camera
    /// - `transparent`: Whether the object is transparent
    #[must_use]
    pub fn new(
        pipeline_id: RenderPipelineId,
        material_index: u32,
        depth: f32,
        transparent: bool,
    ) -> Self {
        // 1. Compress depth into 30 bits.
        // Note: assumes depth >= 0.0. Clamping negative values to 0 is safe.
        let d_u32 = if depth.is_sign_negative() {
            0
        } else {
            depth.to_bits() >> 2
        };
        let raw_d_bits = u64::from(d_u32) & 0x3FFF_FFFF;

        // 2. Prepare other fields
        let p_bits = u64::from(pipeline_id.0 & 0x3FFF); // 14 bits
        let m_bits = u64::from(material_index & 0xFFFFF); // 20 bits

        if transparent {
            // [Transparent]: Sort by Depth (far→near) > Pipeline > Material

            // 1. Invert depth so farther objects (larger depth) get smaller values, sorting first.
            let d_bits = raw_d_bits ^ 0x3FFF_FFFF;

            // 2. Bit layout: Depth (30) << 34 | Pipeline (14) << 20 | Material (20)
            Self((d_bits << 34) | (p_bits << 20) | m_bits)
        } else {
            // [Opaque]: Sort by Pipeline > Material > Depth (near→far)

            // Depth in ascending order (smaller depth first = front-to-back)
            let d_bits = raw_d_bits;

            // Pipeline (14) << 50 | Material (20) << 30 | Depth (30)
            Self((p_bits << 50) | (m_bits << 30) | d_bits)
        }
    }
}

// ============================================================================
// RenderFrame
// ============================================================================

/// Render frame manager.
///
/// Uses a render graph architecture:
/// 1. **Extract**: Pull rendering data from the scene
/// 2. **Prepare**: Prepare GPU resources
/// 3. **Execute**: Run render passes via [`FrameComposer`]
///
/// # Performance Notes
/// - `ExtractedScene` is persistent to reuse memory across frames
/// - `FrameComposer` is created per-frame but extremely cheap (just pointer ops)
///
/// # Note
/// `RenderLists` is stored in `RendererState` rather than here
/// to avoid borrow-checker limitations.
pub struct RenderFrame {
    pub(crate) render_state: RenderState,
    pub(crate) extracted_scene: ExtractedScene,
}

impl Default for RenderFrame {
    fn default() -> Self {
        Self::new()
    }
}

impl RenderFrame {
    #[must_use]
    pub fn new() -> Self {
        Self {
            render_state: RenderState::new(),
            extracted_scene: ExtractedScene::with_capacity(1024),
        }
    }

    /// Returns a reference to the render state.
    #[inline]
    pub fn render_state(&self) -> &RenderState {
        &self.render_state
    }

    /// Returns a reference to the extracted scene data.
    #[inline]
    pub fn extracted_scene(&self) -> &ExtractedScene {
        &self.extracted_scene
    }

    /// Extract scene data, build shadow views, and prepare global GPU resources.
    ///
    /// # Phases
    ///
    /// 1. **Extract** — Pull rendering data from the [`Scene`] into
    ///    [`ExtractedScene`].
    /// 2. **Shadow View Generation** — Build [`RenderView`]s for all
    ///    shadow-casting lights (pure math, no GPU work).
    /// 3. **Shadow Metadata** — Write per-light shadow layer indices,
    ///    cascade matrices and split distances into the light storage
    ///    buffer so the global bind group already contains correct data.
    /// 4. **Global Prepare** — Upload camera / scene / light uniforms and
    ///    create the global bind group (Group 0).
    ///
    /// # Note
    ///
    /// Surface acquisition is deferred to `FrameComposer::render()` to
    /// minimise swap-chain buffer hold time.
    pub fn extract_and_prepare(
        &mut self,
        resource_manager: &mut ResourceManager,
        scene: &mut Scene,
        camera: &RenderCamera,
        assets: &AssetServer,
        frame_time: FrameTime,
        render_lists: &mut RenderLists,
        surface_size: (u32, u32),
    ) {
        use crate::core::view::RenderView;

        resource_manager.next_frame();

        // ── 1. Extract ─────────────────────────────────────────────────
        self.extracted_scene
            .extract_into(scene, camera, assets, resource_manager);

        // ── 2. Resolve GPU environment + BRDF LUT ─────────────────────
        let env_max_mip = resource_manager.resolve_gpu_environment(assets, &scene.environment);
        resource_manager.ensure_brdf_lut();

        {
            let current = scene.uniforms_buffer.read().env_map_max_mip_level;
            if (current - env_max_mip).abs() > f32::EPSILON {
                scene.uniforms_buffer.write().env_map_max_mip_level = env_max_mip;
            }
        }

        // ── 3. Build shadow views (pure math) ──────────────────────────
        render_lists.clear();
        render_lists.active_views.push(RenderView::new_main_camera(
            camera.view_projection_matrix,
            camera.frustum,
            surface_size,
        ));

        let shadow_views = Self::build_shadow_views(
            &self.extracted_scene,
            camera,
            render_lists.active_views.len(),
        );

        // ── 4. Shadow map allocation is deferred to RDG ─────────────────
        // The physical shadow texture is now a transient RDG resource,
        // allocated by the graph compiler after topology compilation.
        // No `ensure_shadow_maps()` call is needed here.

        render_lists.active_views.extend(shadow_views);

        // ── 5. Update light storage buffer with shadow metadata ────────
        Self::update_light_shadow_metadata(
            &render_lists.active_views,
            &self.extracted_scene.lights,
            scene,
            resource_manager,
        );

        // ── 6. Global GPU resources ────────────────────────────────────
        self.render_state.update(camera, frame_time);
        resource_manager.prepare_global(assets, scene, &self.render_state);
    }

    /// Build [`RenderView`]s for all shadow-casting lights.
    ///
    /// Returns a `Vec` of shadow views. Each directional light may produce
    /// multiple cascade views; spot lights produce a single view; point
    /// lights are reserved for future cubemap shadows.
    fn build_shadow_views(
        extracted_scene: &ExtractedScene,
        camera: &RenderCamera,
        _existing_view_count: usize,
    ) -> Vec<crate::core::view::RenderView> {
        use myth_scene::light::LightKind;

        // Compute scene caster extent (for CSM Z extension)
        let camera_pos: glam::Vec3 = camera.position.to_array().into();
        let mut max_distance = 0.0f32;
        for item in &extracted_scene.render_items {
            if !item.cast_shadows {
                continue;
            }
            let aabb = item.world_aabb;
            let effective_radius = if aabb.is_finite() {
                aabb.size().length() * 0.5
            } else {
                0.0
            };
            let center_ws = aabb.center();
            let distance = camera_pos.distance(center_ws) + effective_radius;
            max_distance = max_distance.max(distance);
        }
        let scene_caster_extent = max_distance.max(50.0);

        let mut shadow_views = Vec::with_capacity(16);

        for (light_buffer_index, light) in extracted_scene.lights.iter().enumerate() {
            if !light.cast_shadows {
                continue;
            }
            let shadow_cfg = light.shadow.clone().unwrap_or_default();

            match &light.kind {
                LightKind::Directional(_) => {
                    let cam_far = if camera.far.is_finite() {
                        camera.far
                    } else {
                        shadow_cfg.max_shadow_distance
                    };
                    let shadow_far = shadow_cfg.max_shadow_distance.min(cam_far);
                    let caster_extension = scene_caster_extent.max(shadow_cfg.max_shadow_distance);
                    let base_layer = shadow_views.len() as u32;

                    let (views, _splits) = shadow_utils::build_directional_views(
                        light.id,
                        light.direction,
                        light_buffer_index,
                        camera,
                        &shadow_cfg,
                        shadow_far,
                        caster_extension,
                        base_layer,
                    );
                    shadow_views.extend(views);
                }
                LightKind::Spot(spot) => {
                    let base_layer = shadow_views.len() as u32;
                    shadow_views.push(shadow_utils::build_spot_view(
                        light.id,
                        light_buffer_index,
                        light.position,
                        light.direction,
                        spot,
                        &shadow_cfg,
                        base_layer,
                    ));
                }
                LightKind::Point(_) => {
                    // Future: build 6 cubemap face views
                }
            }
        }

        shadow_views
    }

    /// Write per-light shadow metadata (layer indices, cascade matrices,
    /// bias values) into the scene's light storage buffer.
    ///
    /// Also populates `render_lists.shadow_lights` for the GPU shadow pass.
    fn update_light_shadow_metadata(
        active_views: &[crate::core::view::RenderView],
        extracted_lights: &[crate::graph::extracted::ExtractedLight],
        scene: &mut Scene,
        resource_manager: &mut ResourceManager,
    ) {
        use crate::core::view::ViewTarget;
        use crate::graph::shadow_utils::MAX_CASCADES;
        use glam::{Mat4, Vec4};

        // Reset shadow fields
        {
            let mut light_storage = scene.light_storage_buffer.write();
            for light in light_storage.iter_mut() {
                light.shadow_layer_index = -1;
                light.shadow_matrices.0 = [Mat4::IDENTITY; 4];
                light.cascade_count = 0;
                light.cascade_splits = Vec4::ZERO;
            }
        }

        let total_layers = active_views.iter().filter(|v| v.is_shadow()).count() as u32;

        if total_layers == 0 {
            resource_manager.ensure_buffer(&scene.light_storage_buffer);
            return;
        }

        // Aggregate per-light shadow metadata
        {
            let mut light_storage = scene.light_storage_buffer.write();

            for (light_buffer_index, light) in extracted_lights.iter().enumerate() {
                if !light.cast_shadows {
                    continue;
                }
                let shadow_cfg = light.shadow.clone().unwrap_or_default();

                let mut base_layer = u32::MAX;
                let mut cascade_count = 0u32;
                let mut cascade_matrices = [Mat4::IDENTITY; MAX_CASCADES as usize];
                let mut cascade_splits_arr = [0.0f32; MAX_CASCADES as usize];

                for view in active_views {
                    let ViewTarget::ShadowLight {
                        light_id,
                        layer_index,
                    } = view.target
                    else {
                        continue;
                    };
                    if light_id != light.id {
                        continue;
                    }
                    if layer_index < base_layer {
                        base_layer = layer_index;
                    }
                    cascade_count += 1;
                }

                if cascade_count == 0 {
                    continue;
                }

                for view in active_views {
                    let ViewTarget::ShadowLight {
                        light_id,
                        layer_index,
                    } = view.target
                    else {
                        continue;
                    };
                    if light_id != light.id {
                        continue;
                    }
                    let cascade_idx = (layer_index - base_layer) as usize;
                    if cascade_idx < MAX_CASCADES as usize {
                        cascade_matrices[cascade_idx] = view.view_projection;
                        if let Some(split) = view.csm_split {
                            cascade_splits_arr[cascade_idx] = split;
                        }
                    }
                }

                if let Some(gpu_light) = light_storage.get_mut(light_buffer_index) {
                    gpu_light.shadow_layer_index = base_layer.cast_signed();
                    gpu_light.shadow_matrices.0 = cascade_matrices;
                    gpu_light.cascade_count = cascade_count;
                    gpu_light.cascade_splits = Vec4::new(
                        cascade_splits_arr[0],
                        cascade_splits_arr[1.min(cascade_count as usize - 1)],
                        cascade_splits_arr[2.min(cascade_count as usize - 1)],
                        cascade_splits_arr[3.min(cascade_count as usize - 1)],
                    );
                    gpu_light.shadow_bias = shadow_cfg.bias;
                    gpu_light.shadow_normal_bias = shadow_cfg.normal_bias;
                }
            }
        }

        resource_manager.ensure_buffer(&scene.light_storage_buffer);
    }

    /// Periodically prune stale resources.
    pub fn maybe_prune(&self, resource_manager: &mut ResourceManager) {
        // Periodic cleanup (TODO: LRU eviction strategy)
        if resource_manager.frame_index().is_multiple_of(600) {
            resource_manager.prune(6000);
        }
    }
}
