//! [`RenderPipeline`]: a data-driven, ordered list of composable render passes.
//!
//! Stage 4's renderer drew everything in one hardcoded pass. Stage 5 generalizes
//! that into a list of named [`RenderPass`]es that share one frame's targets and
//! run in order. A project enables only the passes it needs — this is the
//! mechanism behind *scalable fidelity*: a flat unlit/low-poly look (or a
//! stylized post effect) versus a full realistic stack, paying only for the
//! passes turned on.
//!
//! The Stage-4 forward pass is retrofitted onto this as [`ForwardPass`], so the
//! default pipeline ([`RenderPipeline::forward`]) is just `[Clear, Forward]` and
//! produces pixel-identical output. Later stages add passes (shadows,
//! post-process, overlay UI) **without touching the renderer core** — they
//! register a pass.

use crate::math::{Color, Rect, Transform, Vec2};

use super::{clear_view, Camera, ForwardRenderer, Lighting, RenderObject};

/// Everything one frame's passes operate on: the shared color target and the
/// scene view to draw.
///
/// Passes share the same `color` target (and, as the pipeline grows, depth and
/// intermediate textures), which is what makes them *composable*: a clear pass
/// fills the target, the forward pass draws into it, a future post pass reads and
/// rewrites it.
pub struct FrameContext<'a> {
    /// The GPU device.
    pub device: &'a wgpu::Device,
    /// The GPU queue.
    pub queue: &'a wgpu::Queue,
    /// The color target every pass renders into.
    pub color: &'a wgpu::TextureView,
    /// Target size in physical pixels (the whole color target the pipeline
    /// is writing into).
    pub size: (u32, u32),
    /// The sub-rectangle of the target that drawing is restricted to, in
    /// physical pixels (`min` = upper-left, `max` = lower-right). Passes
    /// configure the wgpu viewport from this and the camera uses its
    /// aspect ratio for the projection.
    ///
    /// `None` means "use the full target" — the default for headless tests
    /// and for hosts that render to a whole window. The editor sets this to
    /// the Viewport tab's rect from the docking shell so picking and
    /// projection align with what the user sees inside the tab rather than
    /// stretching across the whole window.
    pub viewport_rect: Option<Rect>,
    /// The background clear color (used by [`ClearPass`]).
    pub clear_color: Color,
    /// The camera to render from.
    pub camera: &'a Camera,
    /// The camera's world placement.
    pub view_transform: &'a Transform,
    /// Scene lighting.
    pub lighting: &'a Lighting,
    /// The drawables, already culled by the host (e.g. by camera
    /// [`visibility`](Camera::visibility)).
    pub objects: &'a [RenderObject<'a>],
}

impl FrameContext<'_> {
    /// The viewport rect [`viewport_rect`](Self::viewport_rect) resolves to —
    /// the explicit sub-rect when set, otherwise the full target.
    pub fn resolved_viewport(&self) -> Rect {
        self.viewport_rect.unwrap_or_else(|| {
            Rect::from_min_size(
                Vec2::ZERO,
                Vec2::new(self.size.0.max(1) as f32, self.size.1.max(1) as f32),
            )
        })
    }
}

/// One stage of the frame. Implement this to add a custom pass; register it on a
/// [`RenderPipeline`]. Passes are owned by the pipeline and run in order.
pub trait RenderPass {
    /// Records this pass's GPU work for the frame.
    fn run(&mut self, frame: &mut FrameContext<'_>);
}

struct PassEntry {
    name: String,
    enabled: bool,
    pass: Box<dyn RenderPass>,
}

/// An ordered, named list of render passes.
///
/// Add passes with [`add_pass`](Self::add_pass), toggle them with
/// [`set_enabled`](Self::set_enabled), or drop them with [`remove`](Self::remove)
/// — all without touching any pass's implementation. [`render`](Self::render)
/// runs every enabled pass in order against one [`FrameContext`].
#[derive(Default)]
pub struct RenderPipeline {
    passes: Vec<PassEntry>,
}

impl RenderPipeline {
    /// An empty pipeline (no passes).
    pub fn new() -> Self {
        Self::default()
    }

    /// The default forward pipeline: a [`ClearPass`] followed by a
    /// [`ForwardPass`]. Pixel-identical to the Stage-4 renderer's output.
    pub fn forward(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
        let mut pipeline = Self::new();
        pipeline.add_pass("clear", ClearPass);
        pipeline.add_pass("forward", ForwardPass::new(device, color_format));
        pipeline
    }

    /// Appends a named pass (enabled). Replaces any existing pass with the same
    /// name, keeping its position.
    pub fn add_pass(&mut self, name: impl Into<String>, pass: impl RenderPass + 'static) {
        let name = name.into();
        let entry = PassEntry {
            name: name.clone(),
            enabled: true,
            pass: Box::new(pass),
        };
        match self.passes.iter_mut().find(|e| e.name == name) {
            Some(existing) => *existing = entry,
            None => self.passes.push(entry),
        }
    }

    /// Inserts a pass before the pass named `before` (or at the end if not
    /// found). Useful for slotting a post effect into a fixed position.
    pub fn insert_before(
        &mut self,
        before: &str,
        name: impl Into<String>,
        pass: impl RenderPass + 'static,
    ) {
        let entry = PassEntry {
            name: name.into(),
            enabled: true,
            pass: Box::new(pass),
        };
        match self.passes.iter().position(|e| e.name == before) {
            Some(index) => self.passes.insert(index, entry),
            None => self.passes.push(entry),
        }
    }

    /// Enables or disables the named pass. Returns whether it exists.
    pub fn set_enabled(&mut self, name: &str, enabled: bool) -> bool {
        match self.passes.iter_mut().find(|e| e.name == name) {
            Some(entry) => {
                entry.enabled = enabled;
                true
            }
            None => false,
        }
    }

    /// Removes the named pass. Returns whether it existed.
    pub fn remove(&mut self, name: &str) -> bool {
        let before = self.passes.len();
        self.passes.retain(|e| e.name != name);
        self.passes.len() != before
    }

    /// Whether a pass with this name is registered.
    pub fn has_pass(&self, name: &str) -> bool {
        self.passes.iter().any(|e| e.name == name)
    }

    /// The pass names in execution order.
    pub fn pass_names(&self) -> impl Iterator<Item = &str> {
        self.passes.iter().map(|e| e.name.as_str())
    }

    /// Runs every enabled pass in order against `frame`.
    pub fn render(&mut self, frame: &mut FrameContext<'_>) {
        for entry in &mut self.passes {
            if entry.enabled {
                entry.pass.run(frame);
            }
        }
    }
}

/// A pass that clears the color target to [`FrameContext::clear_color`].
///
/// Conventionally the first pass, so later passes load over the cleared
/// background (matching the Stage-4 clear-then-draw flow).
pub struct ClearPass;

impl RenderPass for ClearPass {
    fn run(&mut self, frame: &mut FrameContext<'_>) {
        clear_view(frame.device, frame.queue, frame.color, frame.clear_color);
    }
}

/// A pass that draws the frame's objects with the lit forward renderer.
///
/// Wraps the Stage-4 [`ForwardRenderer`]; the color target is *loaded* (so a
/// preceding [`ClearPass`] shows through), depth is managed internally.
pub struct ForwardPass {
    renderer: ForwardRenderer,
}

impl ForwardPass {
    /// Builds a forward pass for the given color target format.
    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
        Self {
            renderer: ForwardRenderer::new(device, color_format),
        }
    }

    /// The wrapped renderer's color format.
    pub fn color_format(&self) -> wgpu::TextureFormat {
        self.renderer.color_format()
    }
}

impl RenderPass for ForwardPass {
    fn run(&mut self, frame: &mut FrameContext<'_>) {
        self.renderer.render(
            frame.device,
            frame.queue,
            frame.color,
            frame.size,
            frame.resolved_viewport(),
            frame.camera,
            frame.view_transform,
            frame.lighting,
            frame.objects,
        );
    }
}
