use bevy::{
    color::palettes::css::YELLOW,
    mesh::{Indices, PrimitiveTopology, VertexAttributeValues},
    prelude::*,
};
use bevy_egui::input::EguiWantsInput;
use bevy_normal_material::prelude::NormalMaterial;
use curvo::prelude::{
    AdaptiveTessellationNode, AdaptiveTessellationOptions, CompoundCurve3D, DividableDirection,
    NurbsCurve3D, NurbsSurface3D, SurfaceTessellation3D, Tessellation,
};
use nalgebra::U4;

use crate::LineMaterial;

pub enum CurveVariant<'a> {
    NurbsCurve(&'a NurbsCurve3D<f64>),
    CompoundCurve(&'a CompoundCurve3D<f64>),
}

impl<'a> From<&'a NurbsCurve3D<f64>> for CurveVariant<'a> {
    fn from(curve: &'a NurbsCurve3D<f64>) -> Self {
        CurveVariant::NurbsCurve(curve)
    }
}

impl<'a> From<&'a CompoundCurve3D<f64>> for CurveVariant<'a> {
    fn from(curve: &'a CompoundCurve3D<f64>) -> Self {
        CurveVariant::CompoundCurve(curve)
    }
}

#[allow(unused)]
pub fn add_curve<'a, C: Into<CurveVariant<'a>>>(
    curve: C,
    color: Option<Color>,
    tolerance: Option<f64>,
    commands: &mut Commands<'_, '_>,
    meshes: &mut ResMut<'_, Assets<Mesh>>,
    line_materials: &mut ResMut<'_, Assets<LineMaterial>>,
) {
    let curve: CurveVariant<'a> = curve.into();

    let samples = match curve {
        CurveVariant::NurbsCurve(n) => n.tessellate(tolerance),
        CurveVariant::CompoundCurve(c) => c
            .spans()
            .iter()
            .flat_map(|span| span.tessellate(tolerance))
            .collect(),
    };

    let line_vertices: Vec<_> = samples
        .iter()
        .map(|p| p.cast::<f32>())
        .map(|p| p.into())
        .collect();
    let n = line_vertices.len();
    let mut line = Mesh::new(PrimitiveTopology::LineStrip, default()).with_inserted_attribute(
        Mesh::ATTRIBUTE_POSITION,
        VertexAttributeValues::Float32x3(line_vertices),
    );
    if color.is_none() {
        line.insert_attribute(
            Mesh::ATTRIBUTE_COLOR,
            VertexAttributeValues::Float32x4(
                (0..n)
                    .map(|i| Color::hsl(((i as f32) / n as f32) * 300., 0.5, 0.5))
                    .map(|c| c.to_srgba().to_f32_array())
                    .collect(),
            ),
        );
    }
    commands.spawn((
        Mesh3d(meshes.add(line)),
        MeshMaterial3d(line_materials.add(LineMaterial {
            color: color.unwrap_or(Color::WHITE),
            ..Default::default()
        })),
    ));
}

#[allow(unused)]
pub fn add_regular_curve<'a, C: Into<CurveVariant<'a>>>(
    curve: C,
    color: Option<Color>,
    divs: usize,
    commands: &mut Commands<'_, '_>,
    meshes: &mut ResMut<'_, Assets<Mesh>>,
    line_materials: &mut ResMut<'_, Assets<LineMaterial>>,
) {
    let curve: CurveVariant<'a> = curve.into();

    let samples = match curve {
        CurveVariant::NurbsCurve(n) => {
            n.sample_regular_range(n.knots_domain().0, n.knots_domain().1, divs)
        }
        CurveVariant::CompoundCurve(c) => c
            .spans()
            .iter()
            .flat_map(|span| {
                span.sample_regular_range(span.knots_domain().0, span.knots_domain().1, divs)
            })
            .collect(),
    };

    let line_vertices: Vec<_> = samples
        .iter()
        .map(|p| p.cast::<f32>())
        .map(|p| p.into())
        .collect();
    let n = line_vertices.len();
    let mut line = Mesh::new(PrimitiveTopology::LineStrip, default()).with_inserted_attribute(
        Mesh::ATTRIBUTE_POSITION,
        VertexAttributeValues::Float32x3(line_vertices),
    );
    if color.is_none() {
        line.insert_attribute(
            Mesh::ATTRIBUTE_COLOR,
            VertexAttributeValues::Float32x4(
                (0..n)
                    .map(|i| Color::hsl(((i as f32) / n as f32) * 300., 0.5, 0.5))
                    .map(|c| c.to_srgba().to_f32_array())
                    .collect(),
            ),
        );
    }
    commands.spawn((
        Mesh3d(meshes.add(line)),
        MeshMaterial3d(line_materials.add(LineMaterial {
            color: color.unwrap_or(Color::WHITE),
            ..Default::default()
        })),
    ));
}

#[allow(unused)]
pub fn surface_2_mesh(
    surface: &NurbsSurface3D<f64>,
    option: Option<AdaptiveTessellationOptions<f64, U4>>,
) -> Mesh {
    let option = option.unwrap_or_default();
    let tess = surface.tessellate(Some(option));
    let tess = tess.cast::<f32>();

    let vertices = tess.points().iter().map(|pt| (*pt).into()).collect();
    let normals = tess.normals().iter().map(|n| (*n).into()).collect();
    let uvs = tess.uvs().iter().map(|uv| (*uv).into()).collect();
    let indices = tess
        .faces()
        .iter()
        .flat_map(|f| f.iter().map(|i| *i as u32))
        .collect();

    Mesh::new(PrimitiveTopology::TriangleList, default())
        .with_inserted_attribute(
            Mesh::ATTRIBUTE_POSITION,
            VertexAttributeValues::Float32x3(vertices),
        )
        .with_inserted_attribute(
            Mesh::ATTRIBUTE_NORMAL,
            VertexAttributeValues::Float32x3(normals),
        )
        .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs))
        .with_inserted_indices(Indices::U32(indices))
}

#[allow(unused)]
pub fn surface_2_regular_mesh(surface: &NurbsSurface3D<f64>, divs_u: usize, divs_v: usize) -> Mesh {
    let tess = surface.regular_tessellate(divs_u, divs_v);
    let tess = tess.cast::<f32>();

    let vertices = tess.points().iter().map(|pt| (*pt).into()).collect();
    let normals = tess.normals().iter().map(|n| (*n).into()).collect();
    let uvs = tess.uvs().iter().map(|uv| (*uv).into()).collect();
    let indices = tess
        .faces()
        .iter()
        .flat_map(|f| f.iter().map(|i| *i as u32))
        .collect();

    Mesh::new(PrimitiveTopology::TriangleList, default())
        .with_inserted_attribute(
            Mesh::ATTRIBUTE_POSITION,
            VertexAttributeValues::Float32x3(vertices),
        )
        .with_inserted_attribute(
            Mesh::ATTRIBUTE_NORMAL,
            VertexAttributeValues::Float32x3(normals),
        )
        .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs))
        .with_inserted_indices(Indices::U32(indices))
}

#[allow(unused)]
pub fn add_surface<F>(
    surface: &NurbsSurface3D<f64>,
    commands: &mut Commands<'_, '_>,
    meshes: &mut ResMut<'_, Assets<Mesh>>,
    normal_materials: &mut ResMut<'_, Assets<NormalMaterial>>,
    option: Option<AdaptiveTessellationOptions<f64, U4, F>>,
) where
    F: Fn(&AdaptiveTessellationNode<f64, U4>) -> Option<DividableDirection> + Copy,
{
    let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, default());

    let option = option.unwrap_or_default();
    let tess = surface.tessellate(Some(option));
    let tess = tess.cast::<f32>();

    let vertices = tess.points().iter().map(|pt| (*pt).into()).collect();
    let normals = tess.normals().iter().map(|n| (*n).into()).collect();
    let uvs = tess.uvs().iter().map(|uv| (*uv).into()).collect();
    let indices = tess
        .faces()
        .iter()
        .flat_map(|f| f.iter().map(|i| *i as u32))
        .collect();

    mesh.insert_attribute(
        Mesh::ATTRIBUTE_POSITION,
        VertexAttributeValues::Float32x3(vertices),
    );
    mesh.insert_attribute(
        Mesh::ATTRIBUTE_NORMAL,
        VertexAttributeValues::Float32x3(normals),
    );
    mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs));
    mesh.insert_indices(Indices::U32(indices));

    commands.spawn((
        Mesh3d(meshes.add(mesh)),
        MeshMaterial3d(normal_materials.add(NormalMaterial {
            cull_mode: None,
            ..Default::default()
        })),
    ));
}

#[allow(unused)]
pub fn add_surface_normals(
    tess: &SurfaceTessellation3D<f32>,
    commands: &mut Commands<'_, '_>,
    meshes: &mut ResMut<'_, Assets<Mesh>>,
    line_materials: &mut ResMut<'_, Assets<LineMaterial>>,
) {
    let mut line_list = Mesh::new(bevy::mesh::PrimitiveTopology::LineList, default());
    let normal_length = 0.15;
    let normals = tess.normals();

    let vertices = tess
        .points()
        .iter()
        .enumerate()
        .flat_map(|(i, p)| {
            let pt: Vec3 = (*p).into();
            let normal: Vec3 = normals[i].normalize().into();
            [pt, pt + normal * normal_length]
        })
        .map(|p| p.to_array())
        .collect();

    line_list.insert_attribute(
        Mesh::ATTRIBUTE_POSITION,
        VertexAttributeValues::Float32x3(vertices),
    );

    commands
        .spawn((
            Mesh3d(meshes.add(line_list)),
            MeshMaterial3d(line_materials.add(LineMaterial {
                color: YELLOW.into(),
                ..Default::default()
            })),
        ))
        .insert(Name::new("normal"));
}

#[allow(unused)]
pub fn absorb_egui_inputs(
    mut mouse: ResMut<ButtonInput<MouseButton>>,
    egui_wants_input: Res<EguiWantsInput>,
) {
    if egui_wants_input.is_pointer_over_area() {
        mouse.reset_all();
    }
}
