use crate::diagnostics::PrepareError;
use crate::geometry::{GeometryTopology, Primitive, PrimitiveVertexAttributes, Vertex};
use crate::material::MaterialKind;
use crate::render::camera::CameraProjection;

use super::cpu_bake::{
    CpuBakeCorner, baked_shadow_visibility, cpu_texture_subdivisions, push_material_pass_primitive,
    subdivided_cpu_corners,
};
use super::lighting::{MaterialShadingInput, material_color};
use super::materials::{
    anisotropy_texture_sample, base_color_texture_sample, clearcoat_normal_texture_sample,
    clearcoat_roughness_texture_sample, clearcoat_texture_sample, emissive_texture_sample,
    iridescence_texture_sample, iridescence_thickness_texture_sample, material_pass,
    metallic_roughness_texture_sample, multiply_color, normal_texture_sample,
    occlusion_texture_sample, render_material_slot, sheen_color_texture_sample,
    sheen_roughness_texture_sample, thickness_texture_sample, transmission_texture_sample,
};
use super::strokes;
use super::tangents::{accumulate_vertex_tangents, authored_vertex_tangents};
use super::transforms::{
    normal_from_model_matrix, transform_normal, transform_position, world_from_model_matrix,
};
use super::types::{
    DeformationInputs, GeometryPrimitiveSource, PrimitiveBakeParams, PrimitiveSinks,
};

pub(super) fn append_geometry_primitives<F>(
    source: GeometryPrimitiveSource<'_, F>,
    deformation: DeformationInputs<'_>,
    params: PrimitiveBakeParams<'_>,
    sinks: PrimitiveSinks<'_>,
) -> Result<(), PrepareError> {
    match source.geometry.topology() {
        GeometryTopology::Triangles => {
            append_triangle_primitives(source, deformation, params, sinks)
        }
        GeometryTopology::Lines => strokes::append_line_primitives(
            source.node,
            source.geometry,
            source.material,
            params.target,
            sinks.primitives,
        ),
    }
}

fn append_triangle_primitives<F>(
    source: GeometryPrimitiveSource<'_, F>,
    deformation: DeformationInputs<'_>,
    params: PrimitiveBakeParams<'_>,
    mut sinks: PrimitiveSinks<'_>,
) -> Result<(), PrepareError> {
    match source.material.kind() {
        MaterialKind::Unlit | MaterialKind::PbrMetallicRoughness => {}
        MaterialKind::Line => {
            return Err(PrepareError::UnsupportedMaterialKind {
                node: source.node,
                kind: source.material.kind(),
            });
        }
        MaterialKind::Wireframe => {
            return strokes::append_wireframe_primitives(
                source.node,
                source.geometry,
                source.material,
                params.target,
                sinks.primitives,
            );
        }
        MaterialKind::Edge => {
            return strokes::append_edge_primitives(
                source.node,
                source.geometry,
                source.material,
                params.target,
                sinks.primitives,
            );
        }
    }

    let material_pass = material_pass(source.node, source.material)?;
    let morphed_vertices = deformation
        .morph_weights
        .and_then(|weights| source.geometry.morphed_vertices(weights));
    let base_vertices = morphed_vertices
        .as_deref()
        .unwrap_or_else(|| source.geometry.vertices());
    let skinned_vertices = match deformation.skin_matrices {
        Some(matrices) => source
            .geometry
            .skinned_vertices(base_vertices, matrices)
            .map_err(|error| PrepareError::InvalidSkinGeometry {
                node: source.node,
                reason: format!("{error:?}"),
            })?,
        None if source.geometry.skin().is_some() => {
            return Err(PrepareError::InvalidSkinGeometry {
                node: source.node,
                reason: "skinned geometry is missing a scene skin binding".to_string(),
            });
        }
        None => None,
    };
    let vertices = skinned_vertices.as_deref().unwrap_or(base_vertices);
    let tex_coords0 = source.geometry.tex_coords0();
    let vertex_tangents =
        authored_vertex_tangents(source.geometry.tangents(), vertices, params.transform)
            .unwrap_or_else(|| {
                accumulate_vertex_tangents(
                    vertices,
                    source.geometry.indices(),
                    tex_coords0,
                    params.transform,
                    params.origin_shift,
                )
            });
    let world_from_model = world_from_model_matrix(params.transform, params.origin_shift);
    let normal_from_model = normal_from_model_matrix(params.transform);

    for triangle in source.geometry.indices().chunks_exact(3) {
        let position_a = transform_position(
            vertices[triangle[0] as usize].position,
            params.transform,
            params.origin_shift,
        );
        let position_b = transform_position(
            vertices[triangle[1] as usize].position,
            params.transform,
            params.origin_shift,
        );
        let position_c = transform_position(
            vertices[triangle[2] as usize].position,
            params.transform,
            params.origin_shift,
        );
        let geometric_normal_a =
            transform_normal(vertices[triangle[0] as usize].normal, params.transform);
        let geometric_normal_b =
            transform_normal(vertices[triangle[1] as usize].normal, params.transform);
        let geometric_normal_c =
            transform_normal(vertices[triangle[2] as usize].normal, params.transform);
        let vertex_colors = source.geometry.vertex_colors();
        let uv_a = tex_coords0[triangle[0] as usize];
        let uv_b = tex_coords0[triangle[1] as usize];
        let uv_c = tex_coords0[triangle[2] as usize];
        let tangent_a = vertex_tangents[triangle[0] as usize];
        let tangent_b = vertex_tangents[triangle[1] as usize];
        let tangent_c = vertex_tangents[triangle[2] as usize];
        let render_material_slot =
            render_material_slot(source.material_handle, params.backend_material_slots);
        let backend_shaded_material = render_material_slot != 0;
        let shadow_visibility_a = baked_shadow_visibility(
            position_a,
            params.lights,
            params.shadow_occluders,
            backend_shaded_material,
        );
        let shadow_visibility_b = baked_shadow_visibility(
            position_b,
            params.lights,
            params.shadow_occluders,
            backend_shaded_material,
        );
        let shadow_visibility_c = baked_shadow_visibility(
            position_c,
            params.lights,
            params.shadow_occluders,
            backend_shaded_material,
        );
        let shade_vertex = |corner: CpuBakeCorner| {
            if backend_shaded_material {
                corner.vertex_color
            } else {
                let normal = normal_texture_sample(
                    source.assets,
                    source.material,
                    corner.uv,
                    corner.geometric_normal,
                );
                let clearcoat_normal = clearcoat_normal_texture_sample(
                    source.assets,
                    source.material,
                    corner.uv,
                    normal,
                );
                multiply_color(
                    material_color(
                        source.material,
                        params.lights,
                        &MaterialShadingInput {
                            position: corner.position,
                            normal,
                            tangent: corner.tangent,
                            tangent_handedness: corner.tangent_handedness,
                            camera_position: params
                                .camera_projection
                                .map(CameraProjection::camera_position),
                            base_color_texture: base_color_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                                params.backend_sampled_base_color_textures,
                            ),
                            metallic_roughness_texture: metallic_roughness_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            occlusion_texture: occlusion_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            emissive_texture: emissive_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            clearcoat_texture: clearcoat_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            clearcoat_roughness_texture: clearcoat_roughness_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            clearcoat_normal,
                            sheen_color_texture: sheen_color_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            sheen_roughness_texture: sheen_roughness_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            anisotropy_texture: anisotropy_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            iridescence_texture: iridescence_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            iridescence_thickness_texture: iridescence_thickness_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            transmission_texture: transmission_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            thickness_texture: thickness_texture_sample(
                                source.assets,
                                source.material,
                                corner.uv,
                            ),
                            environment: params.environment_lighting.clone(),
                            directional_shadow_factor: corner.shadow_visibility,
                        },
                    ),
                    corner.vertex_color,
                )
            }
        };
        let corners = [
            CpuBakeCorner {
                position: position_a,
                geometric_normal: geometric_normal_a,
                uv: uv_a,
                tangent: tangent_a.tangent,
                tangent_handedness: tangent_a.handedness,
                vertex_color: vertex_colors[triangle[0] as usize],
                shadow_visibility: shadow_visibility_a,
            },
            CpuBakeCorner {
                position: position_b,
                geometric_normal: geometric_normal_b,
                uv: uv_b,
                tangent: tangent_b.tangent,
                tangent_handedness: tangent_b.handedness,
                vertex_color: vertex_colors[triangle[1] as usize],
                shadow_visibility: shadow_visibility_b,
            },
            CpuBakeCorner {
                position: position_c,
                geometric_normal: geometric_normal_c,
                uv: uv_c,
                tangent: tangent_c.tangent,
                tangent_handedness: tangent_c.handedness,
                vertex_color: vertex_colors[triangle[2] as usize],
                shadow_visibility: shadow_visibility_c,
            },
        ];
        let subdivisions = cpu_texture_subdivisions(source.material, backend_shaded_material);
        for sub_triangle in subdivided_cpu_corners(corners, subdivisions) {
            let primitive = Primitive::triangle_with_attributes(
                sub_triangle.map(|corner| Vertex {
                    position: corner.position,
                    color: shade_vertex(corner),
                }),
                sub_triangle.map(|corner| PrimitiveVertexAttributes {
                    normal: corner.geometric_normal,
                    tex_coord0: corner.uv,
                    tangent: corner.tangent,
                    tangent_handedness: corner.tangent_handedness,
                    shadow_visibility: corner.shadow_visibility,
                }),
            )
            .with_render_material_slot(render_material_slot);
            let primitive = primitive.with_world_from_model(world_from_model, normal_from_model);
            push_material_pass_primitive(
                primitive,
                material_pass,
                &mut sinks,
                params.camera_projection,
            );
        }
    }

    Ok(())
}
