//! Convex brush hydration from authored face polygons.

use std::collections::BTreeMap;

use avian3d::prelude::ColliderConstructor;
use bevy::asset::RenderAssetUsages;
use bevy::light::{NotShadowCaster, NotShadowReceiver};
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;

use crate::{
    asset_server_path, authoring_component_active, brush_math::validate_brush,
    AuthoringComponentStates, BrushDesc, BrushFaceDesc, ColliderDesc, InspectorOrder, LevelObject,
    MaterialDesc, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC, COMPONENT_MATERIAL_DESC,
};

use super::materials::material_from_desc;

/// Runtime marker for generated brush render/collider children.
#[derive(Component, Reflect, Default, Debug, Clone, Copy)]
#[reflect(Component, Default, Debug)]
pub struct HydratedBrushMesh;

#[allow(clippy::type_complexity)]
pub fn hydrate_brushes(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    brushes: Query<
        (
            Entity,
            &BrushDesc,
            Option<&MaterialDesc>,
            Option<&ColliderDesc>,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ),
        (
            With<LevelObject>,
            Or<(
                Added<BrushDesc>,
                Changed<BrushDesc>,
                Changed<MaterialDesc>,
                Changed<ColliderDesc>,
                Changed<AuthoringComponentStates>,
                Changed<InspectorOrder>,
            )>,
        ),
    >,
    children: Query<&Children>,
    generated_brushes: Query<(), With<HydratedBrushMesh>>,
) {
    for (entity, brush, material, collider, states, legacy_order) in &brushes {
        despawn_brush_mesh(&mut commands, entity, &children, &generated_brushes);
        if !authoring_component_active(states, legacy_order, COMPONENT_BRUSH_DESC) {
            continue;
        }
        spawn_brush_mesh(
            &mut commands,
            &asset_server,
            &mut meshes,
            &mut materials,
            entity,
            brush,
            material.filter(|_| {
                authoring_component_active(states, legacy_order, COMPONENT_MATERIAL_DESC)
            }),
            collider.filter(|_| {
                authoring_component_active(states, legacy_order, COMPONENT_COLLIDER_DESC)
            }),
        );
    }
}

#[expect(
    clippy::too_many_arguments,
    reason = "hydration keeps Bevy asset stores and authored brush inputs explicit"
)]
pub fn spawn_brush_mesh(
    commands: &mut Commands,
    asset_server: &AssetServer,
    meshes: &mut Assets<Mesh>,
    materials: &mut Assets<StandardMaterial>,
    parent: Entity,
    brush: &BrushDesc,
    material: Option<&MaterialDesc>,
    collider: Option<&ColliderDesc>,
) {
    let report = validate_brush(brush);
    if !report.is_valid() {
        for diagnostic in report.diagnostics {
            warn!(
                "Brush hydration skipped invalid brush on {parent:?}: {}",
                diagnostic.message
            );
        }
        return;
    }
    for group in brush_material_groups(brush) {
        let Some(mesh) = brush_mesh_for_faces(group.faces.iter().copied()) else {
            warn!("Brush hydration skipped invalid face batch on {parent:?}");
            continue;
        };
        let mesh = meshes.add(mesh);
        let material = materials.add(brush_group_material(
            asset_server,
            material,
            group.material_path.as_deref(),
            group.texture_path.as_deref(),
        ));

        let mut brush_child = commands.spawn((
            HydratedBrushMesh,
            Name::new("Hydrated Brush Mesh"),
            Mesh3d(mesh),
            MeshMaterial3d(material),
            Transform::default(),
            Visibility::Visible,
            ChildOf(parent),
        ));
        if !brush.cast_shadows {
            brush_child.insert(NotShadowCaster);
        }
        if !brush.receive_shadows {
            brush_child.insert(NotShadowReceiver);
        }
        if collider.is_some_and(|collider| collider.enabled) {
            brush_child.insert(ColliderConstructor::TrimeshFromMesh);
        }
    }
}

pub fn despawn_brush_mesh(
    commands: &mut Commands,
    entity: Entity,
    children: &Query<&Children>,
    generated_brushes: &Query<(), With<HydratedBrushMesh>>,
) {
    let Ok(child_list) = children.get(entity) else {
        return;
    };
    for child in child_list.iter() {
        if generated_brushes.get(child).is_ok() {
            commands.entity(child).despawn();
        }
    }
}

fn brush_mesh_for_faces<'a>(faces: impl IntoIterator<Item = &'a BrushFaceDesc>) -> Option<Mesh> {
    let mut positions = Vec::<[f32; 3]>::new();
    let mut normals = Vec::<[f32; 3]>::new();
    let mut uvs = Vec::<[f32; 2]>::new();
    let mut indices = Vec::<u32>::new();

    for face in faces {
        append_face(face, &mut positions, &mut normals, &mut uvs, &mut indices)?;
    }
    if positions.is_empty() || indices.is_empty() {
        return None;
    }

    let mut mesh = Mesh::new(
        PrimitiveTopology::TriangleList,
        RenderAssetUsages::default(),
    );
    mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
    mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
    mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
    mesh.insert_indices(Indices::U32(indices));
    if let Err(error) = mesh.generate_tangents() {
        warn!("Brush mesh is not Solari-compatible: failed to generate tangents: {error:?}");
    }
    Some(mesh)
}

#[derive(Debug)]
struct BrushMaterialGroup<'a> {
    material_path: Option<String>,
    texture_path: Option<String>,
    faces: Vec<&'a BrushFaceDesc>,
}

fn brush_material_groups(brush: &BrushDesc) -> Vec<BrushMaterialGroup<'_>> {
    let mut groups = BTreeMap::<(Option<String>, Option<String>), Vec<&BrushFaceDesc>>::new();
    for face in &brush.faces {
        let key = (
            face.material
                .as_ref()
                .and_then(|reference| reference.source_path.clone()),
            face.texture
                .as_ref()
                .and_then(|reference| reference.source_path.clone()),
        );
        groups.entry(key).or_default().push(face);
    }
    groups
        .into_iter()
        .map(
            |((material_path, texture_path), faces)| BrushMaterialGroup {
                material_path,
                texture_path,
                faces,
            },
        )
        .collect()
}

fn brush_group_material(
    asset_server: &AssetServer,
    parent_material: Option<&MaterialDesc>,
    material_path: Option<&str>,
    texture_path: Option<&str>,
) -> StandardMaterial {
    let mut material = material_path
        .and_then(|path| crate::load_resolved_material_from_path(path).ok())
        .map(|(desc, _)| material_from_desc(asset_server, &desc))
        .or_else(|| parent_material.map(|material| material_from_desc(asset_server, material)))
        .unwrap_or_else(default_brush_material);
    if let Some(texture_path) = texture_path {
        material.base_color_texture = Some(asset_server.load(asset_server_path(texture_path)));
    }
    material
}

fn default_brush_material() -> StandardMaterial {
    StandardMaterial {
        base_color: Color::srgb(0.64, 0.68, 0.72),
        perceptual_roughness: 0.8,
        ..default()
    }
}

fn append_face(
    face: &BrushFaceDesc,
    positions: &mut Vec<[f32; 3]>,
    normals: &mut Vec<[f32; 3]>,
    uvs: &mut Vec<[f32; 2]>,
    indices: &mut Vec<u32>,
) -> Option<()> {
    if face.vertices.len() < 3 {
        return None;
    }
    if !face.vertices.iter().all(|vertex| vertex.is_finite()) {
        return None;
    }
    let normal = face.plane.normal.try_normalize()?;
    if !normal.is_finite() {
        return None;
    }
    let base = u32::try_from(positions.len()).ok()?;
    let (u_axis, v_axis) = face_axes(normal);
    for vertex in &face.vertices {
        positions.push([vertex.x, vertex.y, vertex.z]);
        normals.push([normal.x, normal.y, normal.z]);
        let uv = transform_face_uv(
            Vec2::new(vertex.dot(u_axis), vertex.dot(v_axis)),
            face.uv_scale,
            face.uv_rotation,
            face.uv_offset,
        );
        uvs.push([uv.x, uv.y]);
    }

    let flip = (face.vertices[1] - face.vertices[0])
        .cross(face.vertices[2] - face.vertices[0])
        .dot(normal)
        < 0.0;
    for i in 1..(face.vertices.len() - 1) {
        let i = u32::try_from(i).ok()?;
        if flip {
            indices.extend_from_slice(&[base, base + i + 1, base + i]);
        } else {
            indices.extend_from_slice(&[base, base + i, base + i + 1]);
        }
    }
    Some(())
}

fn transform_face_uv(base: Vec2, scale: Vec2, rotation_degrees: f32, offset: Vec2) -> Vec2 {
    let scale = if scale.is_finite() { scale } else { Vec2::ONE };
    let rotation_degrees = if rotation_degrees.is_finite() {
        rotation_degrees
    } else {
        0.0
    };
    let offset = if offset.is_finite() {
        offset
    } else {
        Vec2::ZERO
    };
    let scaled = base * scale;
    if rotation_degrees == 0.0 {
        return scaled + offset;
    }
    let (sin, cos) = rotation_degrees.to_radians().sin_cos();
    Vec2::new(
        scaled.x * cos - scaled.y * sin,
        scaled.x * sin + scaled.y * cos,
    ) + offset
}

fn face_axes(normal: Vec3) -> (Vec3, Vec3) {
    let tangent_seed = if normal.y.abs() > 0.9 {
        Vec3::X
    } else {
        Vec3::Y
    };
    let u = tangent_seed.cross(normal).normalize_or_zero();
    let v = normal.cross(u).normalize_or_zero();
    (u, v)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::BrushDesc;

    #[test]
    fn cube_brush_builds_triangle_mesh() {
        let brush = BrushDesc::default();
        let mesh = brush_mesh_for_faces(brush.faces.iter()).expect("cube brush mesh");
        assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
        assert!(mesh.attribute(Mesh::ATTRIBUTE_POSITION).is_some());
        assert!(mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some());
        assert!(mesh.attribute(Mesh::ATTRIBUTE_UV_0).is_some());
        assert!(mesh.attribute(Mesh::ATTRIBUTE_TANGENT).is_some());
        assert!(matches!(mesh.indices(), Some(Indices::U32(indices)) if indices.len() == 36));
        assert!(mesh.enable_raytracing);
    }

    #[test]
    fn brush_faces_group_by_material_paths() {
        let mut brush = BrushDesc::default();
        brush.faces[0].texture = Some(
            crate::EditorAssetRef::new("texture-a", "texture:source", "Texture A")
                .with_source_path("assets/textures/a.png"),
        );
        brush.faces[1].texture = Some(
            crate::EditorAssetRef::new("texture-a", "texture:source", "Texture A")
                .with_source_path("assets/textures/a.png"),
        );
        brush.faces[2].material = Some(
            crate::EditorAssetRef::new("material-a", "material:source", "Material A")
                .with_source_path("assets/materials/a.ron"),
        );

        let groups = brush_material_groups(&brush);
        assert_eq!(groups.len(), 3);
        assert!(groups.iter().any(|group| group.faces.len() == 3));
        assert!(groups.iter().any(|group| {
            group.texture_path.as_deref() == Some("assets/textures/a.png") && group.faces.len() == 2
        }));
        assert!(groups.iter().any(|group| {
            group.material_path.as_deref() == Some("assets/materials/a.ron")
                && group.faces.len() == 1
        }));
    }

    #[test]
    fn brush_face_uv_transform_applies_scale_rotation_and_offset() {
        let uv = transform_face_uv(
            Vec2::new(2.0, 1.0),
            Vec2::new(2.0, 3.0),
            90.0,
            Vec2::new(0.25, -0.5),
        );

        assert!((uv.x + 2.75).abs() < 0.0001);
        assert!((uv.y - 3.5).abs() < 0.0001);

        let sanitized = transform_face_uv(
            Vec2::new(2.0, 1.0),
            Vec2::new(f32::NAN, 3.0),
            f32::INFINITY,
            Vec2::new(0.25, f32::NAN),
        );
        assert_eq!(sanitized, Vec2::new(2.0, 1.0));
    }

    #[test]
    fn invalid_brush_returns_none() {
        let mut brush = BrushDesc::default();
        brush.faces[0].vertices.clear();
        assert!(brush_mesh_for_faces(brush.faces.iter()).is_none());
    }
}
