//! Offscreen WebGL2 capture for publish-wizard preview images.
//!
//! `PreviewCapture` owns a single offscreen `<canvas>` + `three_d::Context` +
//! standard light rig, lazily created once per wizard session. `render_png`
//! renders one frame and reads it back as PNG bytes via `toDataURL` + `atob`.
//!
//! The wasm-only implementation is gated behind
//! `all(target_family = "wasm", target_os = "unknown")`; a no-op stub on other
//! targets keeps the type resolvable for `Wizard`'s signal field.

use crate::mesh::contract::ModelMesh;

/// Errors that can occur constructing or rendering with a [`PreviewCapture`].
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)]
pub enum PreviewCaptureError {
    #[error("preview capture is not supported on this target")]
    Unsupported,
    #[error("browser window is unavailable")]
    WindowUnavailable,
    #[error("document is unavailable")]
    DocumentUnavailable,
    #[error("could not create an offscreen canvas element")]
    CanvasCreation,
    #[error("WebGL2 context is unavailable")]
    WebGL2Unavailable,
    #[error("could not acquire a WebGL2 context: {0}")]
    Context(String),
    #[error("PNG readback failed: {0}")]
    Readback(String),
}

// ---------------------------------------------------------------------------
// wasm target — real implementation
// ---------------------------------------------------------------------------

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
mod wasm {
    use std::sync::Arc;

    use three_d::context::Context as GlowContext;
    use wasm_bindgen::JsCast;

    use super::PreviewCaptureError;
    use crate::mesh::contract::ModelMesh;
    use crate::viewer::{build_scene_with_viewport, standard_lights};

    pub(super) struct PreviewCaptureInner {
        canvas: web_sys::HtmlCanvasElement,
        context: three_d::Context,
        ambient_light: three_d::AmbientLight,
        key_light: three_d::DirectionalLight,
        fill_light: three_d::DirectionalLight,
    }

    impl PreviewCaptureInner {
        pub(super) fn new() -> Result<Self, PreviewCaptureError> {
            let window = web_sys::window().ok_or(PreviewCaptureError::WindowUnavailable)?;
            let document = window
                .document()
                .ok_or(PreviewCaptureError::DocumentUnavailable)?;
            let canvas = document
                .create_element("canvas")
                .map_err(|_| PreviewCaptureError::CanvasCreation)?
                .dyn_into::<web_sys::HtmlCanvasElement>()
                .map_err(|_| PreviewCaptureError::CanvasCreation)?;

            // `preserveDrawingBuffer: true` is REQUIRED for `toDataURL` to read
            // back a non-blank frame. The viewer's plain `get_context("webgl2")`
            // has no preserve flag and would yield a blank readback.
            let opts = js_sys::Object::new();
            js_sys::Reflect::set(&opts, &"preserveDrawingBuffer".into(), &true.into())
                .map_err(|e| PreviewCaptureError::Context(format!("{e:?}")))?;
            let gl = canvas
                .get_context_with_context_options("webgl2", &opts)
                .map_err(|_| PreviewCaptureError::WebGL2Unavailable)?
                .ok_or(PreviewCaptureError::WebGL2Unavailable)?
                .dyn_into::<web_sys::WebGl2RenderingContext>()
                .map_err(|_| PreviewCaptureError::WebGL2Unavailable)?;

            let glow_context = GlowContext::from_webgl2_context(gl);
            #[allow(
                clippy::arc_with_non_send_sync,
                reason = "three-d requires Arc<glow::Context> for browser WebGL context construction"
            )]
            let context = three_d::Context::from_gl_context(Arc::new(glow_context))
                .map_err(|e| PreviewCaptureError::Context(format!("{e:?}")))?;

            let (ambient_light, key_light, fill_light) = standard_lights(&context);

            Ok(Self {
                canvas,
                context,
                ambient_light,
                key_light,
                fill_light,
            })
        }

        pub(super) fn render_png(
            &self,
            model: &ModelMesh,
            width: u32,
            height: u32,
        ) -> Result<Vec<u8>, PreviewCaptureError> {
            self.canvas.set_width(width);
            self.canvas.set_height(height);

            let (mesh, camera, _control) = build_scene_with_viewport(
                &self.context,
                model,
                three_d::Viewport::new_at_origo(width, height),
            );

            let screen = three_d::RenderTarget::screen(&self.context, width, height);
            let _ = screen
                .clear(three_d::ClearState::color_and_depth(
                    0.035, 0.043, 0.067, 1.0, 1.0,
                ))
                .render(
                    &camera,
                    [&mesh],
                    &[&self.ambient_light, &self.key_light, &self.fill_light],
                );

            // Primary readback: toDataURL → base64 → atob → bytes.
            // `atob` returns a Latin-1 binary string (code units 0x00–0xFF); each
            // Rust `char` is exactly one byte. Iterating `.bytes()` would
            // UTF-8-expand bytes ≥0x80 and silently corrupt the PNG.
            let data_url = self
                .canvas
                .to_data_url_with_type("image/png")
                .map_err(|e| PreviewCaptureError::Readback(format!("toDataURL failed: {e:?}")))?;
            let b64 = data_url.split_once(',').map(|(_, b)| b).ok_or_else(|| {
                PreviewCaptureError::Readback("missing data URL delimiter".into())
            })?;
            let window = web_sys::window().ok_or(PreviewCaptureError::WindowUnavailable)?;
            let binary_string = window
                .atob(b64)
                .map_err(|e| PreviewCaptureError::Readback(format!("atob failed: {e:?}")))?;
            Ok(binary_string.chars().map(|c| c as u8).collect())
        }
    }
}

// ---------------------------------------------------------------------------
// non-wasm target — stub
// ---------------------------------------------------------------------------

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
#[allow(dead_code)]
pub(super) struct PreviewCaptureInner;

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
#[allow(dead_code)]
impl PreviewCaptureInner {
    pub(super) fn new() -> Result<Self, PreviewCaptureError> {
        Err(PreviewCaptureError::Unsupported)
    }

    pub(super) fn render_png(
        &self,
        _model: &ModelMesh,
        _width: u32,
        _height: u32,
    ) -> Result<Vec<u8>, PreviewCaptureError> {
        Err(PreviewCaptureError::Unsupported)
    }
}

// ---------------------------------------------------------------------------
// Public cfg-agnostic facade
// ---------------------------------------------------------------------------

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use wasm::PreviewCaptureInner;

/// An offscreen WebGL2 capture surface for rendering preview images.
///
/// On wasm this owns a real `<canvas>` + `three_d::Context`; on every other
/// target it is a no-op stub whose `new()` always returns `Err(Unsupported)`.
/// The wasm inner is wrapped in `Arc` so cloning is cheap without requiring
/// the GL resources themselves to be `Clone`.
#[derive(Clone)]
pub struct PreviewCapture {
    #[allow(dead_code)]
    inner: std::sync::Arc<PreviewCaptureInner>,
}

#[allow(dead_code)] // stub methods are unused on non-wasm targets
impl PreviewCapture {
    /// Create a capture surface. On non-wasm targets this always fails with
    /// [`PreviewCaptureError::Unsupported`].
    #[allow(
        clippy::arc_with_non_send_sync,
        reason = "three-d requires Arc<glow::Context> for browser WebGL context construction"
    )]
    pub fn new() -> Result<Self, PreviewCaptureError> {
        Ok(Self {
            inner: std::sync::Arc::new(PreviewCaptureInner::new()?),
        })
    }

    /// Render `model` to a PNG byte vector at the given dimensions.
    pub fn render_png(
        &self,
        model: &ModelMesh,
        width: u32,
        height: u32,
    ) -> Result<Vec<u8>, PreviewCaptureError> {
        self.inner.render_png(model, width, height)
    }
}
