//! Window surface rendering: swapchain configuration, resize, clear loop.

use std::sync::Arc;

use winit::window::Window;

use super::{clear_view, Gpu, RenderError};
use crate::math::Color;
use crate::window::RenderCtx;

/// Renders to a window surface.
///
/// Owns the [`Gpu`] plus the window's [`wgpu::Surface`] and its
/// configuration. Stage 2 scope: every frame is cleared to
/// [`clear_color`](Self::clear_color); draw passes come in later stages.
pub struct RenderContext {
    gpu: Gpu,
    surface: wgpu::Surface<'static>,
    config: wgpu::SurfaceConfiguration,
    clear_color: Color,
}

impl RenderContext {
    /// Acquires the GPU and configures a surface for `window`.
    ///
    /// The window is held by `Arc` so the surface (which borrows it) can be
    /// `'static`, as winit hands out windows from its event loop.
    ///
    /// To run on any device, several render backends are tried in turn — the
    /// default (env-selected Vulkan/Metal/DX12), then GL, then a software
    /// adapter — and the first that produces a *configurable* surface wins.
    /// This is what lets the engine survive drivers that report a GPU but
    /// cannot present to the window's surface (e.g. old NVIDIA on Wayland under
    /// Vulkan, where `surface.configure` would otherwise fail).
    pub fn new(window: Arc<Window>) -> Result<Self, RenderError> {
        // (label, backend override, force a software adapter)
        let attempts: [(&str, Option<wgpu::Backends>, bool); 3] = [
            ("default", None, false),
            ("GL", Some(wgpu::Backends::GL), false),
            ("software", None, true),
        ];

        let mut last_err: Option<RenderError> = None;
        for (i, &(label, backends, force_fallback)) in attempts.iter().enumerate() {
            match Self::try_backend(&window, backends, force_fallback) {
                Ok(ctx) => {
                    if i > 0 {
                        log::warn!("render backend fell back to '{label}'");
                    }
                    return Ok(ctx);
                }
                Err(err) => {
                    log::warn!("render backend '{label}' unavailable: {err}");
                    last_err = Some(err);
                }
            }
        }
        Err(last_err.unwrap_or(RenderError::NoWorkingBackend))
    }

    /// Attempts one backend: build an instance (optionally forcing `backends`),
    /// create the surface, acquire an adapter/device (optionally a software
    /// one), and configure the surface. Any failure returns `Err` so the caller
    /// can try the next backend rather than aborting the process.
    fn try_backend(
        window: &Arc<Window>,
        backends: Option<wgpu::Backends>,
        force_fallback_adapter: bool,
    ) -> Result<Self, RenderError> {
        let size = window.inner_size();
        // The window doubles as the display handle (needed by GL/X11-style
        // backends); `from_env` keeps backend/flags overridable via WGPU_*.
        let mut desc =
            wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(window.clone()));
        if let Some(backends) = backends {
            desc.backends = backends;
        }
        let instance = wgpu::Instance::new(desc);
        let surface = instance.create_surface(window.clone())?;
        let gpu = Gpu::with_instance(instance, Some(&surface), force_fallback_adapter)?;

        let config = surface
            .get_default_config(gpu.adapter(), size.width.max(1), size.height.max(1))
            .ok_or(RenderError::UnsupportedSurface)?;
        configure_surface(gpu.device(), &surface, &config)?;
        log::info!(
            "surface configured: {}x{} {:?} ({:?}) on {:?}",
            config.width,
            config.height,
            config.format,
            config.present_mode,
            gpu.adapter().get_info().backend,
        );

        Ok(Self {
            gpu,
            surface,
            config,
            clear_color: Color::BLACK,
        })
    }

    /// Reconfigures the surface for a new window size. Zero dimensions
    /// (minimized window) are clamped to 1 so the surface stays valid.
    pub fn resize(&mut self, width: u32, height: u32) {
        self.config.width = width.max(1);
        self.config.height = height.max(1);
        self.surface.configure(self.gpu.device(), &self.config);
    }

    /// Current surface size in physical pixels.
    pub fn size(&self) -> (u32, u32) {
        (self.config.width, self.config.height)
    }

    /// The surface's texture format. Apps need this to build render pipelines
    /// (or UI integrations) whose output matches the surface.
    pub fn surface_format(&self) -> wgpu::TextureFormat {
        self.config.format
    }

    /// The color the surface is cleared to each frame.
    pub fn clear_color(&self) -> Color {
        self.clear_color
    }

    /// Sets the clear color; takes effect on the next rendered frame.
    pub fn set_clear_color(&mut self, color: Color) {
        self.clear_color = color;
    }

    /// Renders one frame: acquires the next surface texture, clears it, and
    /// presents. Equivalent to [`render_frame_with`](Self::render_frame_with)
    /// with an empty draw hook.
    pub fn render_frame(&mut self, window: &Window) -> Result<(), RenderError> {
        self.render_frame_with(window, |_| {})
    }

    /// Renders one frame, invoking `draw` after the clear and before present.
    ///
    /// The surface texture is acquired and cleared to
    /// [`clear_color`](Self::clear_color), then `draw` is handed a
    /// [`RenderCtx`] so it can record additional passes into the same view
    /// (use `LoadOp::Load` to preserve the clear), and finally the frame is
    /// presented.
    ///
    /// Lost or outdated surfaces (e.g. mid-resize) are reconfigured and the
    /// frame skipped; timed-out or occluded acquires skip the frame. All are
    /// normal transient conditions and not reported as errors.
    pub fn render_frame_with(
        &mut self,
        window: &Window,
        draw: impl FnOnce(&RenderCtx<'_>),
    ) -> Result<(), RenderError> {
        use wgpu::CurrentSurfaceTexture;
        let frame = match self.surface.get_current_texture() {
            // A suboptimal frame is still presentable; the next resize event
            // reconfigures the surface anyway.
            CurrentSurfaceTexture::Success(frame) | CurrentSurfaceTexture::Suboptimal(frame) => {
                frame
            }
            CurrentSurfaceTexture::Lost | CurrentSurfaceTexture::Outdated => {
                self.surface.configure(self.gpu.device(), &self.config);
                return Ok(());
            }
            CurrentSurfaceTexture::Timeout | CurrentSurfaceTexture::Occluded => return Ok(()),
            CurrentSurfaceTexture::Validation => return Err(RenderError::SurfaceValidation),
        };
        let view = frame
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        clear_view(self.gpu.device(), self.gpu.queue(), &view, self.clear_color);

        let ctx = RenderCtx {
            gpu: &self.gpu,
            view: &view,
            window,
            surface_format: self.config.format,
            size: (self.config.width, self.config.height),
        };
        draw(&ctx);

        frame.present();
        Ok(())
    }

    /// The underlying GPU handle.
    pub fn gpu(&self) -> &Gpu {
        &self.gpu
    }
}

/// Configures `surface`, capturing any validation error instead of letting it
/// reach wgpu's default (fatal, process-aborting) error handler.
///
/// `surface.configure` returns `()` and reports failures through the device's
/// error sink, which by default panics. Wrapping it in a validation error scope
/// turns "Invalid surface" (and similar) into a recoverable [`Result`] so the
/// caller can fall back to another backend.
fn configure_surface(
    device: &wgpu::Device,
    surface: &wgpu::Surface<'static>,
    config: &wgpu::SurfaceConfiguration,
) -> Result<(), RenderError> {
    let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
    surface.configure(device, config);
    // `pop()` consumes the guard and yields any captured error. On native
    // backends the future is already resolved; `block_on` just unwraps it.
    if let Some(err) = pollster::block_on(scope.pop()) {
        return Err(RenderError::SurfaceConfigure(err.to_string()));
    }
    Ok(())
}
