pub mod camera;
pub mod camera3;
pub mod diff;
pub mod gpu;
pub mod pick;
pub mod pipelines;
pub mod preview;
pub mod scene;
pub mod snapshot;
pub mod surface;

pub use camera::{Camera2, GridSpacing, PixelsPerMm, ViewportExtent, ViewportPx};
pub use camera3::{
    ViewportPoint, clip_from_world, frame_isometric, orbit_about_pixel, orbit_about_point,
    pan_pixels, world_from_clip, world_on_focal_plane, world_ray, zoom_about_pixel,
};
pub use diff::{PixelDiff, PixelDiffError, PixelDiffReport, PixelDiffThreshold, PixelMismatch};
pub use gpu::{BackendTag, Capabilities, Gpu, OffscreenContext};
pub use pick::{
    EntityKindTag, PickAperture, PickId, PickIdError, PickIndex, PickQuery, PickedItem, Picker,
};
pub use pipelines::{
    ArcPipeline, ChromeInstance, ChromePipeline, ChromeTextPipeline, GlyphPipeline, GridPipeline,
    LinesPipeline, SdfGlyphInstance, SolidPipeline, TextPipeline,
};
pub use preview::{PreviewArc, PreviewCircle, SketchPreview};
pub use scene::{
    RelationGlyphKind, SceneArc, SceneCircle, SceneDimension, SceneLine, ScenePoint,
    SceneRelationGlyph, SketchScene, SolidScene,
};
pub use snapshot::{
    ClearColor, GlyphStyle, GridStyle, SnapshotFrame, StrokeStyle, Style, TextStyle, decode_png,
    encode_png,
};
pub use surface::{SurfaceContext, SurfaceError};

#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_precision_loss,
    reason = "GPU upload narrows f64 geometry to f32 through this single funnel"
)]
pub(crate) fn lower_f32(value: f64) -> f32 {
    value as f32
}

#[derive(Copy, Clone)]
pub struct RenderTargets<'a> {
    pub color: &'a wgpu::TextureView,
    pub pick: &'a wgpu::TextureView,
}

impl<'a> RenderTargets<'a> {
    #[must_use]
    pub const fn new(color: &'a wgpu::TextureView, pick: &'a wgpu::TextureView) -> Self {
        Self { color, pick }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum RenderError {
    #[error("no wgpu adapter matched the offscreen request: {0}")]
    NoAdapter(#[from] wgpu::RequestAdapterError),
    #[error("wgpu device request failed: {0}")]
    Device(#[from] wgpu::RequestDeviceError),
    #[error("wgpu poll failed: {0}")]
    Poll(wgpu::PollError),
    #[error("wgpu buffer map failed: {0}")]
    Map(wgpu::BufferAsyncError),
    #[error("wgpu buffer map callback did not fire after poll")]
    MapMissing,
    #[error("png encode failed: {0}")]
    PngEncode(#[from] png::EncodingError),
    #[error("png decode failed: {0}")]
    PngDecode(#[from] png::DecodingError),
    #[error("png format unsupported: color={color_type:?}, depth={bit_depth:?}, require rgba8")]
    PngFormat {
        color_type: png::ColorType,
        bit_depth: png::BitDepth,
    },
    #[error("viewport dimension is zero")]
    ZeroExtent,
    #[error("pick id construction failed: {0}")]
    PickId(#[from] PickIdError),
    #[error("pick query {query} outside viewport {extent}")]
    PickOutOfBounds {
        query: PickQuery,
        extent: ViewportExtent,
    },
    #[error("camera lowering failed: {0}")]
    Camera(#[from] bone_types::TypesError),
}

pub type Result<T, E = RenderError> = core::result::Result<T, E>;

#[derive(Debug)]
pub struct SketchRenderer {
    grid: GridPipeline,
    arcs: ArcPipeline,
    lines: LinesPipeline,
    glyphs: GlyphPipeline,
    text: TextPipeline,
}

impl SketchRenderer {
    #[must_use]
    pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self {
        Self {
            grid: GridPipeline::new(gpu, color_format),
            arcs: ArcPipeline::new(gpu, color_format),
            lines: LinesPipeline::new(gpu, color_format),
            glyphs: GlyphPipeline::new(gpu, color_format),
            text: TextPipeline::new(gpu, color_format),
        }
    }

    pub fn prepare(&mut self, scene: &SketchScene, style: &Style) {
        self.text.prepare(scene, style);
    }

    #[must_use]
    pub fn text_cache_len(&self) -> usize {
        self.text.cache_len()
    }

    pub fn encode_passes(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        targets: RenderTargets<'_>,
        scene: &SketchScene,
        preview: &SketchPreview,
        camera: Camera2,
        style: &Style,
    ) {
        self.grid.draw(encoder, targets.color, camera, style);
        gpu::clear_pick_attachment(encoder, targets.pick);
        self.arcs
            .draw(encoder, targets, camera, style, scene, preview);
        self.lines
            .draw(encoder, targets, camera, style, scene, preview);
        self.glyphs.draw(encoder, targets, camera, style, scene);
        self.text.draw(encoder, targets, camera, style, scene);
    }

    pub fn render(
        &mut self,
        ctx: &OffscreenContext,
        scene: &SketchScene,
        camera: Camera2,
        style: &Style,
    ) -> Result<SnapshotFrame> {
        self.render_with_preview(ctx, scene, &SketchPreview::empty(), camera, style)
    }

    pub fn render_with_preview(
        &mut self,
        ctx: &OffscreenContext,
        scene: &SketchScene,
        preview: &SketchPreview,
        camera: Camera2,
        style: &Style,
    ) -> Result<SnapshotFrame> {
        debug_assert_eq!(
            camera.extent(),
            ctx.extent(),
            "camera extent must match offscreen context extent",
        );
        self.prepare(scene, style);
        ctx.render(|encoder, color_view, pick_view| {
            self.encode_passes(
                encoder,
                RenderTargets::new(color_view, pick_view),
                scene,
                preview,
                camera,
                style,
            );
        })
    }
}

#[derive(Debug)]
pub struct SolidRenderer {
    solid: SolidPipeline,
    depth: Option<DepthTarget>,
}

#[derive(Debug)]
struct DepthTarget {
    extent: ViewportExtent,
    texture: wgpu::Texture,
}

impl SolidRenderer {
    #[must_use]
    pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self {
        Self {
            solid: SolidPipeline::new(gpu, color_format),
            depth: None,
        }
    }

    pub fn render(
        &mut self,
        ctx: &OffscreenContext,
        scene: &SolidScene,
        camera: bone_types::Camera3,
        style: &Style,
    ) -> Result<SnapshotFrame> {
        let extent = ctx.extent();
        let clip_from_world = camera3::clip_from_world(camera, extent)?;
        if !matches!(&self.depth, Some(target) if target.extent == extent) {
            self.depth = Some(DepthTarget {
                extent,
                texture: depth_texture(ctx.gpu().device(), extent),
            });
        }
        let Some(target) = &self.depth else {
            unreachable!("depth target populated above");
        };
        let depth_view = target
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let solid = &self.solid;
        ctx.render(|encoder, color_view, _pick_view| {
            solid.draw(
                encoder,
                color_view,
                &depth_view,
                scene,
                clip_from_world,
                style,
            );
        })
    }
}

fn depth_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture {
    device.create_texture(&wgpu::TextureDescriptor {
        label: Some("bone-render:solid-depth"),
        size: wgpu::Extent3d {
            width: extent.width().value(),
            height: extent.height().value(),
            depth_or_array_layers: 1,
        },
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: pipelines::solid::DEPTH_FORMAT,
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
        view_formats: &[],
    })
}
