pub mod camera;
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 diff::{PixelDiff, PixelDiffError, PixelDiffReport, PixelDiffThreshold, PixelMismatch};
pub use gpu::{BackendTag, Capabilities, Gpu, OffscreenContext};
pub use pick::{EntityKindTag, PickId, PickIdError, PickIndex, PickQuery, PickedItem, Picker};
pub use pipelines::{
    ArcPipeline, ChromeInstance, ChromePipeline, ChromeTextPipeline, GlyphPipeline, GridPipeline,
    LinesPipeline, SdfGlyphInstance, TextPipeline,
};
pub use preview::{PreviewArc, PreviewCircle, SketchPreview};
pub use scene::{
    RelationGlyphKind, SceneArc, SceneCircle, SceneDimension, SceneLine, ScenePoint,
    SceneRelationGlyph, SketchScene,
};
pub use snapshot::{
    ClearColor, GlyphStyle, GridStyle, SnapshotFrame, StrokeStyle, Style, TextStyle, decode_png,
    encode_png,
};
pub use surface::{SurfaceContext, SurfaceError};

#[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,
    },
}

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,
            );
        })
    }
}
