use wgpu::util::DeviceExt;

use bone_types::{Camera3, Point3, ProjectionKind, UnitVec3, Vec3};

use crate::RenderTargets;
use crate::gpu::{Gpu, PICK_FORMAT};
use crate::lower_f32;
use crate::pick::PickId;
use crate::pipelines::solid::DEPTH_FORMAT;
use crate::scene::{EdgeScene, GenuineEdge, SilhouetteCandidate};

const DEPTH_BIAS_NDC_PER_PX: f32 = 0.001_5;
const GENUINE_OFFSET_PX: f32 = 2.0;
const SILHOUETTE_OFFSET_PX: f32 = 3.0;
const EDGE_HALF_WIDTH_PX: f32 = 0.75;

const STYLE_DASHED: u32 = 1 << 0;
const STYLE_HIDDEN: u32 = 1 << 1;

#[derive(Copy, Clone)]
enum EdgeKind {
    Genuine,
    Silhouette,
    Crease,
}

impl EdgeKind {
    const fn style_bits(self) -> u32 {
        match self {
            Self::Genuine => 0,
            Self::Silhouette => 1 << 2,
            Self::Crease => 2 << 2,
        }
    }
}

#[derive(Copy, Clone)]
enum EdgeLayer {
    Visible,
    Hidden,
}

impl EdgeLayer {
    const fn extra_style_bits(self) -> u32 {
        match self {
            Self::Visible => 0,
            Self::Hidden => STYLE_DASHED | STYLE_HIDDEN,
        }
    }

    const fn depth_compare(self) -> wgpu::CompareFunction {
        match self {
            Self::Visible => wgpu::CompareFunction::LessEqual,
            Self::Hidden => wgpu::CompareFunction::Greater,
        }
    }
}

#[derive(Copy, Clone)]
pub(crate) enum HiddenEdges {
    Omit,
    Dashed,
}

#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct EdgeInstance {
    a: [f32; 3],
    b: [f32; 3],
    half_width_px: f32,
    depth_offset_px: f32,
    style_bits: u32,
    pick_id: u32,
}

const INSTANCE_STRIDE: u64 = core::mem::size_of::<EdgeInstance>() as u64;

#[repr(C, align(16))]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct EdgeFrame {
    clip_from_world: [f32; 16],
    edge_color: [f32; 4],
    hidden_color: [f32; 4],
    viewport_px: [f32; 2],
    dash_period_px: f32,
    dash_on_ratio: f32,
    depth_bias_ndc: f32,
    _pad: [f32; 3],
}

const UNIFORM_SIZE: u64 = core::mem::size_of::<EdgeFrame>() as u64;

const INSTANCE_ATTRS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
    0 => Float32x3,
    1 => Float32x3,
    2 => Float32,
    3 => Float32,
    4 => Uint32,
    5 => Uint32,
];

#[derive(Copy, Clone)]
pub(crate) enum EdgeProjection {
    Orthographic { toward_viewer: Vec3 },
    Perspective { eye: Point3 },
}

impl EdgeProjection {
    #[must_use]
    pub fn from_camera(camera: Camera3) -> Self {
        match camera.projection().kind() {
            ProjectionKind::Orthographic { .. } => Self::Orthographic {
                toward_viewer: camera.eye() - camera.target(),
            },
            ProjectionKind::Perspective { .. } => Self::Perspective { eye: camera.eye() },
        }
    }

    fn view_at(self, point: Point3) -> Vec3 {
        match self {
            Self::Orthographic { toward_viewer } => toward_viewer,
            Self::Perspective { eye } => eye - point,
        }
    }
}

#[derive(Copy, Clone)]
pub(crate) struct EdgeView {
    pub clip_from_world: [f32; 16],
    pub projection: EdgeProjection,
    pub viewport_px: [f32; 2],
    pub crease_threshold_rad: f64,
    pub dash_period_px: f32,
    pub dash_on_ratio: f32,
    pub edge_color: [f32; 4],
    pub hidden_color: [f32; 4],
}

pub(crate) struct Edge3dPipeline {
    device: wgpu::Device,
    queue: wgpu::Queue,
    visible: wgpu::RenderPipeline,
    hidden: wgpu::RenderPipeline,
    uniform_buffer: wgpu::Buffer,
    bind_group: wgpu::BindGroup,
}

impl Edge3dPipeline {
    #[must_use]
    pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self {
        let device = gpu.device().clone();
        let queue = gpu.queue().clone();
        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("bone-render:edge3d-shader"),
            source: wgpu::ShaderSource::Wgsl(include_str!("edge_3d.wgsl").into()),
        });
        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("bone-render:edge3d-bgl"),
            entries: &[wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
                ty: wgpu::BindingType::Buffer {
                    ty: wgpu::BufferBindingType::Uniform,
                    has_dynamic_offset: false,
                    min_binding_size: wgpu::BufferSize::new(UNIFORM_SIZE),
                },
                count: None,
            }],
        });
        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("bone-render:edge3d-layout"),
            bind_group_layouts: &[Some(&bind_group_layout)],
            immediate_size: 0,
        });
        let visible = build_pipeline(
            &device,
            &pipeline_layout,
            &shader,
            color_format,
            EdgeLayer::Visible.depth_compare(),
        );
        let hidden = build_pipeline(
            &device,
            &pipeline_layout,
            &shader,
            color_format,
            EdgeLayer::Hidden.depth_compare(),
        );
        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("bone-render:edge3d-uniform"),
            size: UNIFORM_SIZE,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("bone-render:edge3d-bg"),
            layout: &bind_group_layout,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: uniform_buffer.as_entire_binding(),
            }],
        });
        Self {
            device,
            queue,
            visible,
            hidden,
            uniform_buffer,
            bind_group,
        }
    }

    pub fn draw(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        targets: RenderTargets<'_>,
        depth_view: &wgpu::TextureView,
        scene: &EdgeScene,
        view: EdgeView,
        hidden: HiddenEdges,
    ) {
        let visible = build_instances(scene, view, EdgeLayer::Visible.extra_style_bits());
        let occluded = match hidden {
            HiddenEdges::Dashed => {
                build_instances(scene, view, EdgeLayer::Hidden.extra_style_bits())
            }
            HiddenEdges::Omit => Vec::new(),
        };
        if visible.is_empty() && occluded.is_empty() {
            return;
        }
        let uniform = EdgeFrame {
            clip_from_world: view.clip_from_world,
            edge_color: view.edge_color,
            hidden_color: view.hidden_color,
            viewport_px: view.viewport_px,
            dash_period_px: view.dash_period_px,
            dash_on_ratio: view.dash_on_ratio,
            depth_bias_ndc: DEPTH_BIAS_NDC_PER_PX,
            _pad: [0.0; 3],
        };
        self.queue
            .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
        let visible_buffer = (!visible.is_empty()).then(|| self.instance_buffer(&visible));
        let occluded_buffer = (!occluded.is_empty()).then(|| self.instance_buffer(&occluded));
        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("bone-render:edge3d-pass"),
            color_attachments: &[
                Some(wgpu::RenderPassColorAttachment {
                    view: targets.color,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Load,
                        store: wgpu::StoreOp::Store,
                    },
                }),
                Some(wgpu::RenderPassColorAttachment {
                    view: targets.pick,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Load,
                        store: wgpu::StoreOp::Store,
                    },
                }),
            ],
            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                view: depth_view,
                depth_ops: Some(wgpu::Operations {
                    load: wgpu::LoadOp::Load,
                    store: wgpu::StoreOp::Store,
                }),
                stencil_ops: None,
            }),
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });
        pass.set_bind_group(0, &self.bind_group, &[]);
        if let Some(buffer) = &visible_buffer {
            record_layer(&mut pass, &self.visible, buffer, visible.len());
        }
        if let Some(buffer) = &occluded_buffer {
            record_layer(&mut pass, &self.hidden, buffer, occluded.len());
        }
    }

    fn instance_buffer(&self, instances: &[EdgeInstance]) -> wgpu::Buffer {
        self.device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("bone-render:edge3d-instances"),
                contents: bytemuck::cast_slice(instances),
                usage: wgpu::BufferUsages::VERTEX,
            })
    }
}

fn record_layer(
    pass: &mut wgpu::RenderPass<'_>,
    pipeline: &wgpu::RenderPipeline,
    buffer: &wgpu::Buffer,
    len: usize,
) {
    let Ok(count) = u32::try_from(len) else {
        panic!("edge instance count {len} exceeds u32::MAX");
    };
    pass.set_pipeline(pipeline);
    pass.set_vertex_buffer(0, buffer.slice(..));
    pass.draw(0..6, 0..count);
}

impl core::fmt::Debug for Edge3dPipeline {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Edge3dPipeline").finish_non_exhaustive()
    }
}

fn build_pipeline(
    device: &wgpu::Device,
    layout: &wgpu::PipelineLayout,
    shader: &wgpu::ShaderModule,
    color_format: wgpu::TextureFormat,
    depth_compare: wgpu::CompareFunction,
) -> wgpu::RenderPipeline {
    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: Some("bone-render:edge3d-pipeline"),
        layout: Some(layout),
        vertex: wgpu::VertexState {
            module: shader,
            entry_point: Some("vs"),
            compilation_options: wgpu::PipelineCompilationOptions::default(),
            buffers: &[wgpu::VertexBufferLayout {
                array_stride: INSTANCE_STRIDE,
                step_mode: wgpu::VertexStepMode::Instance,
                attributes: &INSTANCE_ATTRS,
            }],
        },
        fragment: Some(wgpu::FragmentState {
            module: shader,
            entry_point: Some("fs"),
            compilation_options: wgpu::PipelineCompilationOptions::default(),
            targets: &[
                Some(wgpu::ColorTargetState {
                    format: color_format,
                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                }),
                Some(wgpu::ColorTargetState {
                    format: PICK_FORMAT,
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                }),
            ],
        }),
        primitive: wgpu::PrimitiveState {
            topology: wgpu::PrimitiveTopology::TriangleList,
            strip_index_format: None,
            front_face: wgpu::FrontFace::Ccw,
            cull_mode: None,
            polygon_mode: wgpu::PolygonMode::Fill,
            conservative: false,
            unclipped_depth: false,
        },
        depth_stencil: Some(wgpu::DepthStencilState {
            format: DEPTH_FORMAT,
            depth_write_enabled: Some(false),
            depth_compare: Some(depth_compare),
            stencil: wgpu::StencilState::default(),
            bias: wgpu::DepthBiasState::default(),
        }),
        multisample: wgpu::MultisampleState::default(),
        multiview_mask: None,
        cache: None,
    })
}

fn build_instances(scene: &EdgeScene, view: EdgeView, extra_bits: u32) -> Vec<EdgeInstance> {
    let genuine = scene
        .genuine()
        .iter()
        .copied()
        .map(|edge| genuine_instance(edge, view.crease_threshold_rad, extra_bits));
    let silhouette = scene
        .silhouettes()
        .iter()
        .copied()
        .filter(|candidate| straddles(*candidate, view.projection))
        .map(|candidate| silhouette_instance(candidate, extra_bits));
    genuine.chain(silhouette).collect()
}

fn genuine_instance(edge: GenuineEdge, threshold_rad: f64, extra_bits: u32) -> EdgeInstance {
    let kind = if edge.crease().radians() >= threshold_rad {
        EdgeKind::Crease
    } else {
        EdgeKind::Genuine
    };
    instance(
        edge.a(),
        edge.b(),
        edge.pick(),
        kind,
        GENUINE_OFFSET_PX,
        extra_bits,
    )
}

fn silhouette_instance(candidate: SilhouetteCandidate, extra_bits: u32) -> EdgeInstance {
    instance(
        candidate.a(),
        candidate.b(),
        candidate.pick(),
        EdgeKind::Silhouette,
        SILHOUETTE_OFFSET_PX,
        extra_bits,
    )
}

fn instance(
    a: Point3,
    b: Point3,
    pick: PickId,
    kind: EdgeKind,
    depth_offset_px: f32,
    extra_bits: u32,
) -> EdgeInstance {
    EdgeInstance {
        a: lower_point(a),
        b: lower_point(b),
        half_width_px: EDGE_HALF_WIDTH_PX,
        depth_offset_px,
        style_bits: kind.style_bits() | extra_bits,
        pick_id: pick.raw(),
    }
}

fn lower_point(p: Point3) -> [f32; 3] {
    let (x, y, z) = p.coords_mm();
    [lower_f32(x), lower_f32(y), lower_f32(z)]
}

fn straddles(candidate: SilhouetteCandidate, projection: EdgeProjection) -> bool {
    let view = projection.view_at(midpoint(candidate.a(), candidate.b()));
    let front_a = dot_unit_vec(candidate.normal_a(), view);
    let front_b = dot_unit_vec(candidate.normal_b(), view);
    front_a * front_b < 0.0
}

fn midpoint(a: Point3, b: Point3) -> Point3 {
    let (ax, ay, az) = a.coords_mm();
    let (bx, by, bz) = b.coords_mm();
    Point3::from_mm(0.5 * (ax + bx), 0.5 * (ay + by), 0.5 * (az + bz))
}

fn dot_unit_vec(unit: UnitVec3, vec: Vec3) -> f64 {
    let (ux, uy, uz) = unit.components();
    let (vx, vy, vz) = vec.coords_mm();
    ux * vx + uy * vy + uz * vz
}
