use crate::render::{
    Bound, Consts, DebugDrawer, DebugLocals, DebugVertex, Mesh, Model, Quad, Renderer, Tri,
};
use common::util::srgba_to_linear;
use hashbrown::{HashMap, HashSet};
use tracing::warn;
use vek::*;

#[derive(Debug)]
pub enum DebugShape {
    Line([Vec3<f32>; 2]),
    Cylinder {
        radius: f32,
        height: f32,
    },
    CapsulePrism {
        p0: Vec2<f32>,
        p1: Vec2<f32>,
        radius: f32,
        height: f32,
    },
}

impl DebugShape {
    pub fn mesh(&self) -> Mesh<DebugVertex> {
        use core::f32::consts::{PI, TAU};
        let mut mesh = Mesh::new();
        let tri = |x: Vec3<f32>, y: Vec3<f32>, z: Vec3<f32>| {
            Tri::<DebugVertex>::new(x.into(), y.into(), z.into())
        };
        let quad = |x: Vec3<f32>, y: Vec3<f32>, z: Vec3<f32>, w: Vec3<f32>| {
            Quad::<DebugVertex>::new(x.into(), y.into(), z.into(), w.into())
        };

        match self {
            DebugShape::Line([a, b]) => {
                let h = Vec3::new(0.0, 1.0, 0.0);
                mesh.push_quad(quad(*a, a + h, b + h, *b));
            },
            DebugShape::Cylinder { radius, height } => {
                const SUBDIVISIONS: u8 = 16;
                for i in 0..SUBDIVISIONS {
                    // dot on circle edge
                    let to = |n: u8| {
                        let angle = TAU * f32::from(n) / f32::from(SUBDIVISIONS);

                        Vec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
                    };

                    let origin = Vec3::zero();
                    let r0 = to(i);
                    let r1 = to(i + 1);

                    let h = Vec3::new(0.0, 0.0, *height);

                    // Draw bottom sector
                    mesh.push_tri(tri(r1, r0, origin));
                    // Draw face
                    mesh.push_quad(quad(r0, r1, r1 + h, r0 + h));
                    // Draw top sector
                    mesh.push_tri(tri(origin + h, r0 + h, r1 + h));
                }
            },
            DebugShape::CapsulePrism {
                p0,
                p1,
                radius,
                height,
            } => {
                // We split circle in two parts
                const HALF_SECTORS: u8 = 8;
                const TOTAL: u8 = HALF_SECTORS * 2;

                let offset = (p0 - p1).angle_between(Vec2::new(0.0, 1.0));
                let h = Vec3::new(0.0, 0.0, *height);

                let draw_cylinder_sector =
                    |mesh: &mut Mesh<DebugVertex>, origin: Vec3<f32>, from: u8, to: u8| {
                        for i in from..to {
                            // dot on circle edge
                            let to = |n: u8| {
                                let angle = offset + TAU * f32::from(n) / f32::from(TOTAL);
                                let (x, y) = (radius * angle.cos(), radius * angle.sin());
                                let to_edge = Vec3::new(x, y, 0.0);

                                origin + to_edge
                            };

                            let r0 = to(i);
                            let r1 = to(i + 1);

                            // Draw bottom sector
                            mesh.push_tri(tri(r1, r0, origin));
                            // Draw face
                            mesh.push_quad(quad(r0, r1, r1 + h, r0 + h));
                            // Draw top sector
                            mesh.push_tri(tri(origin + h, r0 + h, r1 + h));
                        }
                    };

                let p0 = Vec3::new(p0.x, p0.y, 0.0);
                let p1 = Vec3::new(p1.x, p1.y, 0.0);
                // 1) Draw first half-cylinder
                draw_cylinder_sector(&mut mesh, p0, 0, HALF_SECTORS);

                // 2) Draw cuboid in-between
                // get main line segment
                let a = p1 - p0;
                // normalize
                let a = a / a.magnitude();
                // stretch to radius
                let a = a * *radius;
                // rotate to 90 degrees to get needed shift
                let ortoghonal = Quaternion::rotation_z(PI / 2.0);
                let shift = ortoghonal * a;

                // bottom points
                let a0 = p0 + shift;
                let b0 = p0 - shift;
                let c0 = p1 - shift;
                let d0 = p1 + shift;

                // top points
                let a1 = a0 + h;
                let b1 = b0 + h;
                let c1 = c0 + h;
                let d1 = d0 + h;

                // Bottom
                mesh.push_quad(quad(d0, c0, b0, a0));

                // Faces
                // (we need only two of them, because other two are inside)
                mesh.push_quad(quad(d0, a0, a1, d1));
                mesh.push_quad(quad(b0, c0, c1, b1));

                // Top
                mesh.push_quad(quad(a1, b1, c1, d1));

                // 3) Draw second half-cylinder
                draw_cylinder_sector(&mut mesh, p1, HALF_SECTORS, TOTAL);
            },
        }
        mesh
    }
}

#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub struct DebugShapeId(pub u64);

pub struct Debug {
    next_shape_id: DebugShapeId,
    pending_shapes: HashMap<DebugShapeId, DebugShape>,
    pending_locals: HashMap<DebugShapeId, ([f32; 4], [f32; 4], [f32; 4])>,
    pending_deletes: HashSet<DebugShapeId>,
    models: HashMap<DebugShapeId, (Model<DebugVertex>, Bound<Consts<DebugLocals>>)>,
}

impl Debug {
    pub fn new() -> Debug {
        Debug {
            next_shape_id: DebugShapeId(0),
            pending_shapes: HashMap::new(),
            pending_locals: HashMap::new(),
            pending_deletes: HashSet::new(),
            models: HashMap::new(),
        }
    }

    pub fn add_shape(&mut self, shape: DebugShape) -> DebugShapeId {
        let id = DebugShapeId(self.next_shape_id.0);
        self.next_shape_id.0 += 1;
        self.pending_shapes.insert(id, shape);
        id
    }

    pub fn set_context(&mut self, id: DebugShapeId, pos: [f32; 4], color: [f32; 4], ori: [f32; 4]) {
        self.pending_locals.insert(id, (pos, color, ori));
    }

    pub fn remove_shape(&mut self, id: DebugShapeId) { self.pending_deletes.insert(id); }

    pub fn maintain(&mut self, renderer: &mut Renderer) {
        for (id, shape) in self.pending_shapes.drain() {
            if let Some(model) = renderer.create_model(&shape.mesh()) {
                let locals = renderer.create_debug_bound_locals(&[DebugLocals {
                    pos: [0.0; 4],
                    color: [1.0, 0.0, 0.0, 1.0],
                    ori: [0.0, 0.0, 0.0, 1.0],
                }]);
                self.models.insert(id, (model, locals));
            } else {
                warn!(
                    "Failed to create model for debug shape {:?}: {:?}",
                    id, shape
                );
            }
        }
        for (id, (pos, color, ori)) in self.pending_locals.drain() {
            if let Some((_, locals)) = self.models.get_mut(&id) {
                let lc = srgba_to_linear(color.into());
                let new_locals = [DebugLocals {
                    pos,
                    color: [lc.r, lc.g, lc.b, lc.a],
                    ori,
                }];
                renderer.update_consts(locals, &new_locals);
            } else {
                warn!(
                    "Tried to update locals for nonexistent debug shape {:?}",
                    id
                );
            }
        }
        for id in self.pending_deletes.drain() {
            self.models.remove(&id);
        }
    }

    pub fn render<'a>(&'a self, drawer: &mut DebugDrawer<'_, 'a>) {
        for (model, locals) in self.models.values() {
            drawer.draw(model, locals);
        }
    }
}

impl Default for Debug {
    fn default() -> Debug { Debug::new() }
}
