//! Material and texture processing for FBX files.

use crate::error::FbxError;
use crate::label::FbxAssetLabel;
use crate::loader::FbxLoaderSettings;
use crate::texture::external_texture_reference;
use crate::utils::convert_texture_uv_transform;
use bevy::asset::{Handle, LoadContext};
use bevy::pbr::StandardMaterial;
use bevy::prelude::*;
use bevy_material::AlphaMode;
use bevy_image::{CompressedImageFormats, ImageSampler, ImageType};
use std::collections::{BTreeSet, HashMap};

/// Process all materials from the FBX scene.
pub fn process_materials(
    scene: &ufbx::Scene,
    settings: &FbxLoaderSettings,
    load_context: &mut LoadContext<'_>,
    external_texture_bytes: &HashMap<String, Vec<u8>>,
) -> Result<
    (
        Vec<Handle<StandardMaterial>>,
        HashMap<Box<str>, Handle<StandardMaterial>>,
    ),
    FbxError,
> {
    let mut materials = Vec::new();
    let mut named_materials = HashMap::new();
    let texture_handles = process_textures(
        scene,
        settings,
        load_context,
        external_texture_bytes,
    )?;

    for (index, ufbx_material) in scene.materials.as_ref().iter().enumerate() {
        if ufbx_material.element.element_id == 0 {
            continue;
        }

        let standard_material = create_standard_material(ufbx_material, &texture_handles)?;
        let handle = load_context.add_labeled_asset(
            FbxAssetLabel::Material(index).to_string(),
            standard_material,
        );

        if !ufbx_material.element.name.is_empty() {
            named_materials.insert(
                Box::from(ufbx_material.element.name.as_ref()),
                handle.clone(),
            );
        }

        materials.push(handle);
    }

    Ok((materials, named_materials))
}

/// Process textures from materials.
///
/// Uses the following priority for each texture:
/// 1. Embedded textures (`texture.content` non-empty) — decoded via `Image::from_buffer`
/// 2. External textures via `texture.relative_filename` — already relative to the FBX file
/// 3. External textures via `.fbm` folder extraction from `texture.filename` / `texture.absolute_filename`
pub fn process_textures(
    scene: &ufbx::Scene,
    settings: &FbxLoaderSettings,
    load_context: &mut LoadContext<'_>,
    external_texture_bytes: &HashMap<String, Vec<u8>>,
) -> Result<HashMap<u32, Handle<Image>>, FbxError> {
    let mut texture_handles = HashMap::new();
    let mut external_handles = HashMap::<String, Handle<Image>>::new();
    let mut unavailable = BTreeSet::new();
    let asset_path = load_context.path().clone();

    for (index, texture) in scene.textures.as_ref().iter().enumerate() {
        // Priority 1: Embedded texture data
        if !texture.content.is_empty() {
            let ext = extract_extension(&texture.filename).unwrap_or("png");
            match Image::from_buffer(
                &texture.content,
                ImageType::Extension(ext),
                CompressedImageFormats::NONE,
                true, // is_srgb
                ImageSampler::Default,
                settings.load_materials,
            ) {
                Ok(image) => {
                    let handle = load_context.add_labeled_asset(
                        FbxAssetLabel::Texture(index).to_string(),
                        image,
                    );
                    texture_handles.insert(texture.element.element_id, handle);
                    continue;
                }
                Err(error) => {
                    warn!(
                        "bevy_ufbx: skipped embedded texture {index} for {asset_path}: {error}"
                    );
                    continue;
                }
            }
        }

        let reference = match external_texture_reference(index, texture) {
            Ok(Some(reference)) => reference,
            Ok(None) => continue,
            Err(error) => {
                unavailable.insert(error.to_string());
                continue;
            }
        };
        if let Some(handle) = external_handles.get(&reference.relative_path) {
            texture_handles.insert(reference.element_id, handle.clone());
            continue;
        }

        let Some(bytes) = external_texture_bytes.get(&reference.relative_path) else {
            continue;
        };
        let extension = std::path::Path::new(&reference.relative_path)
            .extension()
            .and_then(|extension| extension.to_str())
            .unwrap_or("png");
        let image = match Image::from_buffer(
            &bytes,
            ImageType::Extension(extension),
            CompressedImageFormats::NONE,
            true,
            ImageSampler::Default,
            settings.load_materials,
        ) {
            Ok(image) => image,
            Err(error) => {
                unavailable.insert(format!("{} ({error})", reference.relative_path));
                continue;
            }
        };
        let handle = load_context
            .add_labeled_asset(FbxAssetLabel::Texture(index).to_string(), image);
        external_handles.insert(reference.relative_path, handle.clone());
        texture_handles.insert(reference.element_id, handle);
    }

    if !unavailable.is_empty() {
        warn!(
            "bevy_ufbx: skipped {} unavailable external texture reference(s) for {}: {}",
            unavailable.len(),
            asset_path,
            unavailable.into_iter().collect::<Vec<_>>().join("; ")
        );
    }

    Ok(texture_handles)
}

/// Extract file extension from a texture filename string.
fn extract_extension(filename: &ufbx::String) -> Option<&str> {
    if filename.is_empty() {
        return None;
    }
    let s = filename.as_ref();
    s.rsplit('.')
        .next()
        .filter(|ext| !ext.contains('/') && !ext.contains('\\'))
}

/// Create a StandardMaterial from ufbx material.
pub fn create_standard_material(
    ufbx_material: &ufbx::Material,
    texture_handles: &HashMap<u32, Handle<Image>>,
) -> Result<StandardMaterial, FbxError> {
    let mut material = StandardMaterial::default();

    // Base color
    if let Ok(diffuse) = std::panic::catch_unwind(|| ufbx_material.fbx.diffuse_color.value_vec4) {
        material.base_color = Color::srgb(diffuse.x as f32, diffuse.y as f32, diffuse.z as f32);
    } else if let Ok(pbr_base) =
        std::panic::catch_unwind(|| ufbx_material.pbr.base_color.value_vec4)
    {
        material.base_color = Color::srgb(pbr_base.x as f32, pbr_base.y as f32, pbr_base.z as f32);
    }

    // Metallic and roughness
    if let Ok(metallic) = std::panic::catch_unwind(|| ufbx_material.pbr.metalness.value_vec4) {
        material.metallic = metallic.x as f32;
    }
    if let Ok(roughness) = std::panic::catch_unwind(|| ufbx_material.pbr.roughness.value_vec4) {
        material.perceptual_roughness = roughness.x as f32;
    }

    // Emission
    if let Ok(emission) = std::panic::catch_unwind(|| ufbx_material.fbx.emission_color.value_vec4) {
        material.emissive =
            LinearRgba::rgb(emission.x as f32, emission.y as f32, emission.z as f32);
    }

    // Alpha
    if ufbx_material.pbr.opacity.value_vec4.x < 1.0 {
        let alpha = ufbx_material.pbr.opacity.value_vec4.x as f32;
        material.alpha_mode = if alpha < 0.98 {
            AlphaMode::Blend
        } else {
            AlphaMode::Opaque
        };
    }

    // Textures
    for texture_ref in &ufbx_material.textures {
        if let Some(image_handle) = texture_handles.get(&texture_ref.texture.element.element_id) {
            match texture_ref.material_prop.as_ref() {
                "DiffuseColor" | "BaseColor" => {
                    material.base_color_texture = Some(image_handle.clone());
                    material.uv_transform = convert_texture_uv_transform(&texture_ref.texture);
                }
                "NormalMap" => material.normal_map_texture = Some(image_handle.clone()),
                "Metallic" => material.metallic_roughness_texture = Some(image_handle.clone()),
                "Roughness" if material.metallic_roughness_texture.is_none() => {
                    material.metallic_roughness_texture = Some(image_handle.clone());
                }
                "EmissiveColor" => material.emissive_texture = Some(image_handle.clone()),
                "AmbientOcclusion" => material.occlusion_texture = Some(image_handle.clone()),
                _ => {}
            }
        }
    }

    Ok(material)
}
