use bone_render::{EdgeScene, GenuineEdge, PickId, SolidScene, Style};
use bone_types::{
    Angle, AxisAngle, Camera3, CreaseAngle, Length, LinearRgba, Point3, Projection, Tolerance,
    UnitVec3, millimeter, radian,
};

const TOL: Tolerance = Tolerance::new(1.0e-9);
const RIGHT_ANGLE: CreaseAngle = CreaseAngle::from_radians(core::f64::consts::FRAC_PI_2);
const CYL_SEGMENTS: u32 = 20;
const TUBE_SEGMENTS: u32 = 14;
const SPHERE_LON: u32 = 20;
const SPHERE_LAT: u32 = 14;

type V3 = [f64; 3];

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct IconMaterial {
    pub color: LinearRgba,
}

impl IconMaterial {
    #[must_use]
    pub const fn new(color: LinearRgba) -> Self {
        Self { color }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Rotation {
    axis: UnitVec3,
    radians: f64,
}

impl Rotation {
    #[must_use]
    pub fn identity() -> Self {
        Self {
            axis: UnitVec3::z_axis(),
            radians: 0.0,
        }
    }

    #[must_use]
    pub fn about(axis: UnitVec3, angle: Angle) -> Self {
        Self {
            axis,
            radians: angle.get::<radian>(),
        }
    }

    #[must_use]
    pub fn about_x(angle: Angle) -> Self {
        Self::about(UnitVec3::x_axis(), angle)
    }

    #[must_use]
    pub fn about_y(angle: Angle) -> Self {
        Self::about(UnitVec3::y_axis(), angle)
    }

    #[must_use]
    pub fn about_z(angle: Angle) -> Self {
        Self::about(UnitVec3::z_axis(), angle)
    }

    fn axis_angle(self) -> AxisAngle {
        AxisAngle::new(self.axis, Angle::new::<radian>(self.radians))
    }

    fn rotate_offset(self, o: V3) -> V3 {
        let rotated =
            Point3::from_mm(o[0], o[1], o[2]).rotated_about(Point3::origin(), self.axis_angle());
        let (x, y, z) = rotated.coords_mm();
        [x, y, z]
    }

    fn rotate_unit(self, u: UnitVec3) -> UnitVec3 {
        u.rotated(self.axis_angle())
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum Prim {
    Box {
        center: [f32; 3],
        half: [f32; 3],
        rotation: Rotation,
        material: IconMaterial,
    },
    Cylinder {
        from: [f32; 3],
        to: [f32; 3],
        radius: f32,
        material: IconMaterial,
    },
    Sphere {
        center: [f32; 3],
        radius: f32,
        material: IconMaterial,
    },
    SweptTube {
        path: Vec<[f32; 3]>,
        radius: f32,
        closed: bool,
        material: IconMaterial,
    },
    Arrow {
        from: [f32; 3],
        to: [f32; 3],
        shaft_radius: f32,
        head_radius: f32,
        head_length: f32,
        material: IconMaterial,
    },
}

impl Prim {
    #[must_use]
    pub fn cuboid(center: [f32; 3], half: [f32; 3], material: IconMaterial) -> Self {
        Self::Box {
            center,
            half,
            rotation: Rotation::identity(),
            material,
        }
    }

    #[must_use]
    pub fn tilted_cuboid(
        center: [f32; 3],
        half: [f32; 3],
        rotation: Rotation,
        material: IconMaterial,
    ) -> Self {
        Self::Box {
            center,
            half,
            rotation,
            material,
        }
    }

    #[must_use]
    pub fn bar(from: [f32; 3], to: [f32; 3], radius: f32, material: IconMaterial) -> Self {
        Self::Cylinder {
            from,
            to,
            radius,
            material,
        }
    }

    #[must_use]
    pub fn ball(center: [f32; 3], radius: f32, material: IconMaterial) -> Self {
        Self::Sphere {
            center,
            radius,
            material,
        }
    }

    #[must_use]
    pub fn loop_tube(path: Vec<[f32; 3]>, radius: f32, material: IconMaterial) -> Self {
        Self::SweptTube {
            path,
            radius,
            closed: true,
            material,
        }
    }

    #[must_use]
    pub fn open_tube(path: Vec<[f32; 3]>, radius: f32, material: IconMaterial) -> Self {
        Self::SweptTube {
            path,
            radius,
            closed: false,
            material,
        }
    }

    #[must_use]
    pub fn arrow(
        from: [f32; 3],
        to: [f32; 3],
        shaft_radius: f32,
        head_radius: f32,
        head_length: f32,
        material: IconMaterial,
    ) -> Self {
        Self::Arrow {
            from,
            to,
            shaft_radius,
            head_radius,
            head_length,
            material,
        }
    }

    fn emit(&self, mesh: &mut MeshBuilder) {
        match self {
            Prim::Box {
                center,
                half,
                rotation,
                material,
            } => emit_cuboid(mesh, *center, *half, *rotation, *material),
            Prim::Cylinder {
                from,
                to,
                radius,
                material,
            } => emit_cylinder(mesh, *from, *to, *radius, *material),
            Prim::Sphere {
                center,
                radius,
                material,
            } => emit_sphere(mesh, *center, *radius, *material),
            Prim::SweptTube {
                path,
                radius,
                closed,
                material,
            } => emit_swept_tube(mesh, path, *radius, *closed, *material),
            Prim::Arrow {
                from,
                to,
                shaft_radius,
                head_radius,
                head_length,
                material,
            } => emit_arrow(
                mesh,
                *from,
                *to,
                *shaft_radius,
                *head_radius,
                *head_length,
                *material,
            ),
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum IconView {
    Iso,
    Front,
}

#[derive(Clone, Debug, PartialEq)]
pub struct IconModel {
    prims: Vec<Prim>,
    view: IconView,
}

impl IconModel {
    #[must_use]
    pub fn new(prims: Vec<Prim>) -> Self {
        Self {
            prims,
            view: IconView::Iso,
        }
    }

    #[must_use]
    pub fn with_view(mut self, view: IconView) -> Self {
        self.view = view;
        self
    }

    #[must_use]
    pub fn view(&self) -> IconView {
        self.view
    }

    #[must_use]
    pub fn tessellate(&self) -> (SolidScene, EdgeScene) {
        let mesh = self
            .prims
            .iter()
            .fold(MeshBuilder::default(), |mut mesh, prim| {
                prim.emit(&mut mesh);
                mesh
            });
        let scene =
            SolidScene::from_parts(mesh.positions, mesh.normals, mesh.colors, mesh.triangles);
        let edges = EdgeScene::from_genuine(mesh.edges);
        (scene, edges)
    }
}

#[derive(Default)]
struct MeshBuilder {
    positions: Vec<Point3>,
    normals: Vec<UnitVec3>,
    colors: Vec<LinearRgba>,
    triangles: Vec<[u32; 3]>,
    edges: Vec<GenuineEdge>,
}

impl MeshBuilder {
    fn base(&self) -> u32 {
        let Ok(base) = u32::try_from(self.positions.len()) else {
            panic!("icon mesh vertex count fits a u32 index");
        };
        base
    }

    fn push_quad(&mut self, corners: [Point3; 4], normal: UnitVec3, color: LinearRgba) {
        self.push_quad_smooth(corners, [normal; 4], color);
    }

    fn push_quad_smooth(
        &mut self,
        corners: [Point3; 4],
        normals: [UnitVec3; 4],
        color: LinearRgba,
    ) {
        let base = self.base();
        self.positions.extend_from_slice(&corners);
        self.normals.extend_from_slice(&normals);
        (0..4).for_each(|_| self.colors.push(color));
        self.triangles.push([base, base + 1, base + 2]);
        self.triangles.push([base, base + 2, base + 3]);
    }

    fn push_tri(&mut self, corners: [Point3; 3], normals: [UnitVec3; 3], color: LinearRgba) {
        let base = self.base();
        self.positions.extend_from_slice(&corners);
        self.normals.extend_from_slice(&normals);
        (0..3).for_each(|_| self.colors.push(color));
        self.triangles.push([base, base + 1, base + 2]);
    }

    fn push_edge(&mut self, a: Point3, b: Point3) {
        self.edges
            .push(GenuineEdge::new(a, b, PickId::NONE, RIGHT_ANGLE));
    }

    fn push_loop_edges(&mut self, ring: &[Point3]) {
        ring.iter().enumerate().for_each(|(i, &a)| {
            let b = ring[(i + 1) % ring.len()];
            self.push_edge(a, b);
        });
    }
}

fn sub(a: V3, b: V3) -> V3 {
    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}

fn add(a: V3, b: V3) -> V3 {
    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}

fn scale(a: V3, s: f64) -> V3 {
    [a[0] * s, a[1] * s, a[2] * s]
}

fn dot(a: V3, b: V3) -> f64 {
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}

fn cross(a: V3, b: V3) -> V3 {
    [
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0],
    ]
}

fn length(a: V3) -> f64 {
    dot(a, a).sqrt()
}

fn normalize(a: V3) -> V3 {
    let n = length(a);
    if n <= f64::EPSILON {
        [0.0, 0.0, 1.0]
    } else {
        scale(a, 1.0 / n)
    }
}

fn vf(a: [f32; 3]) -> V3 {
    [f64::from(a[0]), f64::from(a[1]), f64::from(a[2])]
}

fn pt(v: V3) -> Point3 {
    Point3::from_mm(v[0], v[1], v[2])
}

fn unit(v: V3) -> UnitVec3 {
    let n = normalize(v);
    UnitVec3::new_unchecked(n[0], n[1], n[2])
}

fn perpendicular_frame(dir: V3) -> (V3, V3) {
    let helper = if dir[1].abs() < 0.9 {
        [0.0, 1.0, 0.0]
    } else {
        [1.0, 0.0, 0.0]
    };
    let u = normalize(cross(helper, dir));
    let v = cross(dir, u);
    (u, v)
}

fn ring_points(center: V3, u: V3, v: V3, radius: f64, segments: u32) -> Vec<V3> {
    (0..segments)
        .map(|i| {
            let theta = core::f64::consts::TAU * f64::from(i) / f64::from(segments);
            add(
                center,
                add(
                    scale(u, radius * theta.cos()),
                    scale(v, radius * theta.sin()),
                ),
            )
        })
        .collect()
}

fn emit_cuboid(
    mesh: &mut MeshBuilder,
    center: [f32; 3],
    half: [f32; 3],
    rotation: Rotation,
    material: IconMaterial,
) {
    let c = vf(center);
    let h = vf(half);
    let place = |signs: [f64; 3]| -> Point3 {
        let local = [signs[0] * h[0], signs[1] * h[1], signs[2] * h[2]];
        pt(add(c, rotation.rotate_offset(local)))
    };
    (0..3).for_each(|axis| {
        let u = (axis + 1) % 3;
        let v = (axis + 2) % 3;
        [1.0_f64, -1.0_f64].into_iter().for_each(|sign| {
            let mut axis_dir = [0.0_f64; 3];
            axis_dir[axis] = sign;
            let normal = rotation.rotate_unit(unit(axis_dir));
            let at = |du: f64, dv: f64| {
                let mut signs = [0.0_f64; 3];
                signs[axis] = sign;
                signs[u] = du;
                signs[v] = dv;
                place(signs)
            };
            let p00 = at(-1.0, -1.0);
            let p10 = at(1.0, -1.0);
            let p11 = at(1.0, 1.0);
            let p01 = at(-1.0, 1.0);
            let quad = if sign > 0.0 {
                [p00, p10, p11, p01]
            } else {
                [p00, p01, p11, p10]
            };
            mesh.push_quad(quad, normal, material.color);
        });
    });
    emit_cuboid_edges(mesh, place);
}

fn emit_cuboid_edges(mesh: &mut MeshBuilder, place: impl Fn([f64; 3]) -> Point3) {
    (0..3).for_each(|axis| {
        let u = (axis + 1) % 3;
        let v = (axis + 2) % 3;
        [-1.0_f64, 1.0_f64].into_iter().for_each(|su| {
            [-1.0_f64, 1.0_f64].into_iter().for_each(|sv| {
                let mut lo = [0.0_f64; 3];
                lo[u] = su;
                lo[v] = sv;
                lo[axis] = -1.0;
                let mut hi = lo;
                hi[axis] = 1.0;
                mesh.push_edge(place(lo), place(hi));
            });
        });
    });
}

fn emit_tube_band(
    mesh: &mut MeshBuilder,
    ring_a: &[V3],
    ring_b: &[V3],
    center_a: V3,
    center_b: V3,
    color: LinearRgba,
) {
    let n = ring_a.len();
    (0..n).for_each(|i| {
        let j = (i + 1) % n;
        let a0 = ring_a[i];
        let a1 = ring_a[j];
        let b0 = ring_b[i];
        let b1 = ring_b[j];
        let na0 = unit(sub(a0, center_a));
        let na1 = unit(sub(a1, center_a));
        let nb0 = unit(sub(b0, center_b));
        let nb1 = unit(sub(b1, center_b));
        mesh.push_quad_smooth(
            [pt(a0), pt(a1), pt(b1), pt(b0)],
            [na0, na1, nb1, nb0],
            color,
        );
    });
}

fn emit_cap(mesh: &mut MeshBuilder, ring: &[V3], center: V3, normal: UnitVec3, color: LinearRgba) {
    let n = ring.len();
    let (nx, ny, nz) = normal.components();
    let default_dir = cross(sub(ring[0], center), sub(ring[1], center));
    let flip = dot(default_dir, [nx, ny, nz]) < 0.0;
    (0..n).for_each(|i| {
        let j = (i + 1) % n;
        let (a, b) = if flip {
            (ring[j], ring[i])
        } else {
            (ring[i], ring[j])
        };
        mesh.push_tri([pt(center), pt(a), pt(b)], [normal; 3], color);
    });
}

fn emit_cylinder(
    mesh: &mut MeshBuilder,
    from: [f32; 3],
    to: [f32; 3],
    radius: f32,
    material: IconMaterial,
) {
    let p0 = vf(from);
    let p1 = vf(to);
    let dir = normalize(sub(p1, p0));
    let (u, v) = perpendicular_frame(dir);
    let r = f64::from(radius);
    let ring0 = ring_points(p0, u, v, r, CYL_SEGMENTS);
    let ring1 = ring_points(p1, u, v, r, CYL_SEGMENTS);
    emit_tube_band(mesh, &ring0, &ring1, p0, p1, material.color);
    emit_cap(mesh, &ring0, p0, unit(scale(dir, -1.0)), material.color);
    emit_cap(mesh, &ring1, p1, unit(dir), material.color);
    mesh.push_loop_edges(&ring0.iter().map(|&p| pt(p)).collect::<Vec<_>>());
    mesh.push_loop_edges(&ring1.iter().map(|&p| pt(p)).collect::<Vec<_>>());
}

fn emit_sphere(mesh: &mut MeshBuilder, center: [f32; 3], radius: f32, material: IconMaterial) {
    let c = vf(center);
    let r = f64::from(radius);
    let lat = SPHERE_LAT;
    let lon = SPHERE_LON;
    let point = |i: u32, j: u32| -> (V3, UnitVec3) {
        let phi = core::f64::consts::PI * f64::from(i) / f64::from(lat);
        let theta = core::f64::consts::TAU * f64::from(j) / f64::from(lon);
        let n = [phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin()];
        (add(c, scale(n, r)), unit(n))
    };
    (0..lat).for_each(|i| {
        (0..lon).for_each(|j| {
            let (p00, n00) = point(i, j);
            let (p01, n01) = point(i, j + 1);
            let (p10, n10) = point(i + 1, j);
            let (p11, n11) = point(i + 1, j + 1);
            mesh.push_quad_smooth(
                [pt(p00), pt(p01), pt(p11), pt(p10)],
                [n00, n01, n11, n10],
                material.color,
            );
        });
    });
}

fn path_normal(path: &[V3]) -> V3 {
    let n = path.len();
    let newell = (0..n).fold([0.0_f64; 3], |acc, i| {
        let a = path[i];
        let b = path[(i + 1) % n];
        [
            acc[0] + (a[1] - b[1]) * (a[2] + b[2]),
            acc[1] + (a[2] - b[2]) * (a[0] + b[0]),
            acc[2] + (a[0] - b[0]) * (a[1] + b[1]),
        ]
    });
    if length(newell) <= f64::EPSILON {
        let dir = normalize(sub(path[1.min(n - 1)], path[0]));
        let (u, _) = perpendicular_frame(dir);
        u
    } else {
        normalize(newell)
    }
}

fn tangent_at(path: &[V3], i: usize, closed: bool) -> V3 {
    let n = path.len();
    let prev = if i == 0 {
        if closed { Some(path[n - 1]) } else { None }
    } else {
        Some(path[i - 1])
    };
    let next = if i + 1 == n {
        if closed { Some(path[0]) } else { None }
    } else {
        Some(path[i + 1])
    };
    match (prev, next) {
        (Some(a), Some(b)) => normalize(sub(b, a)),
        (Some(a), None) => normalize(sub(path[i], a)),
        (None, Some(b)) => normalize(sub(b, path[i])),
        (None, None) => [0.0, 0.0, 1.0],
    }
}

fn emit_swept_tube(
    mesh: &mut MeshBuilder,
    path: &[[f32; 3]],
    radius: f32,
    closed: bool,
    material: IconMaterial,
) {
    if path.len() < 2 {
        return;
    }
    let points: Vec<V3> = path.iter().map(|&p| vf(p)).collect();
    let normal = path_normal(&points);
    let r = f64::from(radius);
    let rings: Vec<Vec<V3>> = points
        .iter()
        .enumerate()
        .map(|(i, &center)| {
            let tangent = tangent_at(&points, i, closed);
            let in_plane = normalize(cross(tangent, normal));
            ring_points(center, in_plane, normal, r, TUBE_SEGMENTS)
        })
        .collect();
    let bands = if closed {
        points.len()
    } else {
        points.len() - 1
    };
    (0..bands).for_each(|i| {
        let j = (i + 1) % points.len();
        emit_tube_band(
            mesh,
            &rings[i],
            &rings[j],
            points[i],
            points[j],
            material.color,
        );
    });
    if !closed {
        let last = points.len() - 1;
        let t0 = tangent_at(&points, 0, closed);
        let t1 = tangent_at(&points, last, closed);
        emit_cap(
            mesh,
            &rings[0],
            points[0],
            unit(scale(t0, -1.0)),
            material.color,
        );
        emit_cap(mesh, &rings[last], points[last], unit(t1), material.color);
    }
}

fn emit_arrow(
    mesh: &mut MeshBuilder,
    from: [f32; 3],
    to: [f32; 3],
    shaft_radius: f32,
    head_radius: f32,
    head_length: f32,
    material: IconMaterial,
) {
    let p0 = vf(from);
    let tip = vf(to);
    let dir = normalize(sub(tip, p0));
    let head_len = f64::from(head_length);
    let base = sub(tip, scale(dir, head_len));
    let (frame_u, frame_v) = perpendicular_frame(dir);
    let shaft_r = f64::from(shaft_radius);
    let shaft0 = ring_points(p0, frame_u, frame_v, shaft_r, CYL_SEGMENTS);
    let shaft1 = ring_points(base, frame_u, frame_v, shaft_r, CYL_SEGMENTS);
    emit_tube_band(mesh, &shaft0, &shaft1, p0, base, material.color);
    emit_cap(mesh, &shaft0, p0, unit(scale(dir, -1.0)), material.color);
    let head_r = f64::from(head_radius);
    let base_ring = ring_points(base, frame_u, frame_v, head_r, CYL_SEGMENTS);
    emit_cap(
        mesh,
        &base_ring,
        base,
        unit(scale(dir, -1.0)),
        material.color,
    );
    let count = base_ring.len();
    (0..count).for_each(|i| {
        let j = (i + 1) % count;
        let cap_a = base_ring[i];
        let cap_b = base_ring[j];
        let na = unit(add(scale(dir, head_r), sub(cap_a, base)));
        let nb = unit(add(scale(dir, head_r), sub(cap_b, base)));
        let nt = unit(dir);
        mesh.push_tri(
            [pt(cap_a), pt(cap_b), pt(tip)],
            [na, nb, nt],
            material.color,
        );
    });
    mesh.push_loop_edges(&base_ring.iter().map(|&p| pt(p)).collect::<Vec<_>>());
}

#[must_use]
pub fn icon_camera(view: IconView) -> Camera3 {
    match view {
        IconView::Iso => iso_camera(),
        IconView::Front => front_camera(),
    }
}

fn iso_camera() -> Camera3 {
    let target = Point3::origin();
    let Ok(direction) = UnitVec3::try_from_components(1.0, 0.8, 1.0, TOL) else {
        panic!("icon camera direction is nonzero");
    };
    let eye = target + direction.into_vec(Length::new::<millimeter>(10.0));
    let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(1.5)) else {
        panic!("icon camera half-height is positive");
    };
    let Ok(camera) = Camera3::new(eye, target, UnitVec3::y_axis(), projection) else {
        panic!("icon camera eye and target are 10 mm apart");
    };
    camera
}

fn front_camera() -> Camera3 {
    let target = Point3::origin();
    let eye = target + UnitVec3::z_axis().into_vec(Length::new::<millimeter>(10.0));
    let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(1.3)) else {
        panic!("icon front camera half-height is positive");
    };
    let Ok(camera) = Camera3::new(eye, target, UnitVec3::y_axis(), projection) else {
        panic!("icon front camera eye and target are 10 mm apart");
    };
    camera
}

#[must_use]
pub fn icon_style() -> Style {
    Style::light().with_solid_base_color(LinearRgba::new(1.0, 1.0, 1.0, 1.0))
}
