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

pub use camera::{Camera2, GridSpacing, PixelsPerMm, ViewportExtent, ViewportPx, ViewportRegion};
pub use camera3::{
    ViewportPoint, arcball_rotation, clip_from_world, frame_current, frame_isometric,
    frame_standard_view, frame_view_direction, orbit_about_pixel, orbit_about_point, orbit_pitch,
    orbit_yaw, pan_pixels, roll_about_view, roll_by, 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 navigate::{DragModifiers, NavGesture, ViewportNavigator};
pub use pick::{
    EntityKindTag, PickAperture, PickId, PickIdError, PickIndex, PickQuery, PickedItem, Picker,
};
pub use pipelines::{
    ArcPipeline, ChromeInstance, ChromePipeline, ChromeTextPipeline, ConvexInstance,
    ConvexPolyPipeline, GlyphPipeline, GridPipeline, LinesPipeline, MAX_PLANES, MAX_STROKE_POINTS,
    SdfGlyphInstance, StrokeInstance, StrokePipeline, TextPipeline,
};
pub(crate) use pipelines::{
    Edge3dPipeline, EdgeProjection, EdgeView, FaceFill, HiddenEdges, SolidPipeline, SolidView,
};
pub use preview::{PreviewArc, PreviewCircle, SketchPreview};
pub use scene::{
    EdgeScene, GenuineEdge, RelationGlyphKind, SceneArc, SceneCircle, SceneDimension, SceneLine,
    ScenePoint, SceneRelationGlyph, SilhouetteCandidate, SketchScene, SolidScene,
};
pub use snapshot::{
    ClearColor, EdgeStyle, GlyphStyle, GridStyle, SnapshotFrame, StrokeStyle, Style, TextStyle,
    decode_png, encode_png,
};
pub use surface::{SurfaceContext, SurfaceError};
pub use tween::CameraTween;

#[must_use]
#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_precision_loss,
    reason = "f64 narrows to f32 through this single funnel"
)]
pub 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,
    edges: Edge3dPipeline,
    depth: Option<DepthTarget>,
    shading: bone_types::ShadingModel,
}

#[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),
            edges: Edge3dPipeline::new(gpu, color_format),
            depth: None,
            shading: bone_types::ShadingModel::Gouraud,
        }
    }

    #[must_use]
    pub const fn with_shading_model(mut self, shading: bone_types::ShadingModel) -> Self {
        self.shading = shading;
        self
    }

    pub fn render(
        &mut self,
        ctx: &OffscreenContext,
        scene: &SolidScene,
        camera: bone_types::Camera3,
        style: &Style,
    ) -> Result<SnapshotFrame> {
        self.render_display(
            ctx,
            scene,
            &EdgeScene::empty(),
            camera,
            style,
            bone_types::DisplayMode::ShadedNoEdges,
        )
    }

    pub fn render_display(
        &mut self,
        ctx: &OffscreenContext,
        scene: &SolidScene,
        edges: &EdgeScene,
        camera: bone_types::Camera3,
        style: &Style,
        mode: bone_types::DisplayMode,
    ) -> Result<SnapshotFrame> {
        let extent = ctx.extent();
        let view = SolidFrameView::new(camera, ViewportRegion::at_origin(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());
        ctx.render(|encoder, color_view, pick_view| {
            self.encode_passes(
                encoder,
                RenderTargets::new(color_view, pick_view),
                &depth_view,
                scene,
                edges,
                &SolidDisplay {
                    view: &view,
                    style,
                    mode,
                },
            );
        })
    }

    pub fn encode_passes(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        targets: RenderTargets<'_>,
        depth_view: &wgpu::TextureView,
        scene: &SolidScene,
        edges: &EdgeScene,
        display: &SolidDisplay<'_>,
    ) {
        let &SolidDisplay { view, style, mode } = display;
        let plan = DisplayPlan::for_mode(mode);
        match plan.solid {
            Some(fill) => self.solid.draw(
                encoder,
                targets,
                depth_view,
                scene,
                SolidView {
                    clip_from_world: view.clip_from_world,
                    eye_world: view.eye_world,
                    shading: self.shading,
                    fill,
                    region: view.region,
                },
                style,
            ),
            None => clear_solid_targets(encoder, targets, depth_view, style),
        }
        if plan.edges && !edges.is_empty() {
            let edge_view = EdgeView {
                clip_from_world: view.clip_from_world,
                projection: EdgeProjection::from_camera(view.camera),
                viewport_px: view.viewport_px,
                crease_threshold_rad: CREASE_THRESHOLD_RAD,
                dash_period_px: HIDDEN_DASH_PERIOD_PX,
                dash_on_ratio: HIDDEN_DASH_ON_RATIO,
                edge_color: style.edges().visible().to_rgba_array(),
                hidden_color: style.edges().hidden().to_rgba_array(),
                region: view.region,
            };
            self.edges
                .draw(encoder, targets, depth_view, edges, edge_view, plan.hidden);
        }
    }
}

pub(crate) fn apply_viewport_region(pass: &mut wgpu::RenderPass<'_>, region: ViewportRegion) {
    let (x, y, width, height) = region.scissor();
    if width == 0 || height == 0 {
        return;
    }
    let [vx, vy, vw, vh] = region.viewport();
    pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
    pass.set_scissor_rect(x, y, width, height);
}

pub struct SolidDisplay<'a> {
    pub view: &'a SolidFrameView,
    pub style: &'a Style,
    pub mode: bone_types::DisplayMode,
}

#[derive(Copy, Clone)]
pub struct SolidFrameView {
    camera: bone_types::Camera3,
    clip_from_world: [f32; 16],
    eye_world: [f32; 3],
    viewport_px: [f32; 2],
    region: ViewportRegion,
}

impl SolidFrameView {
    pub fn new(camera: bone_types::Camera3, region: ViewportRegion) -> Result<Self> {
        let extent = region.extent();
        let clip_from_world = camera3::clip_from_world(camera, extent)?;
        let (ex, ey, ez) = camera.eye().coords_mm();
        Ok(Self {
            camera,
            clip_from_world,
            eye_world: [lower_f32(ex), lower_f32(ey), lower_f32(ez)],
            viewport_px: [
                lower_f32(f64::from(extent.width().value())),
                lower_f32(f64::from(extent.height().value())),
            ],
            region,
        })
    }
}

struct DisplayPlan {
    solid: Option<FaceFill>,
    edges: bool,
    hidden: HiddenEdges,
}

impl DisplayPlan {
    const fn for_mode(mode: bone_types::DisplayMode) -> Self {
        use bone_types::DisplayMode;
        match mode {
            DisplayMode::Wireframe => Self {
                solid: None,
                edges: true,
                hidden: HiddenEdges::Omit,
            },
            DisplayMode::HiddenLineRemoved => Self {
                solid: Some(FaceFill::Occluder),
                edges: true,
                hidden: HiddenEdges::Omit,
            },
            DisplayMode::HiddenLineGray => Self {
                solid: Some(FaceFill::Occluder),
                edges: true,
                hidden: HiddenEdges::Dashed,
            },
            DisplayMode::ShadedWithEdges => Self {
                solid: Some(FaceFill::Shaded),
                edges: true,
                hidden: HiddenEdges::Omit,
            },
            DisplayMode::ShadedNoEdges => Self {
                solid: Some(FaceFill::Shaded),
                edges: false,
                hidden: HiddenEdges::Omit,
            },
        }
    }
}

pub(crate) fn solid_clear_color_attachments<'a>(
    targets: RenderTargets<'a>,
    style: &Style,
) -> [Option<wgpu::RenderPassColorAttachment<'a>>; 2] {
    [
        Some(wgpu::RenderPassColorAttachment {
            view: targets.color,
            resolve_target: None,
            depth_slice: None,
            ops: wgpu::Operations {
                load: wgpu::LoadOp::Clear(style.background().into()),
                store: wgpu::StoreOp::Store,
            },
        }),
        Some(wgpu::RenderPassColorAttachment {
            view: targets.pick,
            resolve_target: None,
            depth_slice: None,
            ops: wgpu::Operations {
                load: wgpu::LoadOp::Clear(gpu::pick_clear_color()),
                store: wgpu::StoreOp::Store,
            },
        }),
    ]
}

pub(crate) fn solid_clear_depth_attachment(
    depth_view: &wgpu::TextureView,
) -> wgpu::RenderPassDepthStencilAttachment<'_> {
    wgpu::RenderPassDepthStencilAttachment {
        view: depth_view,
        depth_ops: Some(wgpu::Operations {
            load: wgpu::LoadOp::Clear(1.0),
            store: wgpu::StoreOp::Store,
        }),
        stencil_ops: None,
    }
}

fn clear_solid_targets(
    encoder: &mut wgpu::CommandEncoder,
    targets: RenderTargets<'_>,
    depth_view: &wgpu::TextureView,
    style: &Style,
) {
    let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
        label: Some("bone-render:solid-clear"),
        color_attachments: &solid_clear_color_attachments(targets, style),
        depth_stencil_attachment: Some(solid_clear_depth_attachment(depth_view)),
        timestamp_writes: None,
        occlusion_query_set: None,
        multiview_mask: None,
    });
}

const CREASE_THRESHOLD_RAD: f64 = core::f64::consts::FRAC_PI_6;
const HIDDEN_DASH_PERIOD_PX: f32 = 6.0;
const HIDDEN_DASH_ON_RATIO: f32 = 0.5;

pub(crate) 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: &[],
    })
}
