//! FBX loader implementation for Bevy.

use crate::error::FbxError;
use crate::material::process_materials;
use crate::mesh::process_meshes;
use crate::node::{process_nodes, process_skins};
use crate::scene::build_scene;
use crate::texture::external_texture_path_report;
use crate::types::{Fbx, FbxAxisSystem, FbxMeta, Handedness};
use bevy::asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap};

/// Settings for FBX file loading.
///
/// These settings allow customizing which parts of the FBX file are loaded
/// and how they are processed.
#[derive(Serialize, Deserialize)]
pub struct FbxLoaderSettings {
    /// How meshes should be loaded and used
    pub load_meshes: RenderAssetUsages,
    /// How materials should be loaded and used
    pub load_materials: RenderAssetUsages,
    /// Whether to load cameras from the FBX file
    pub load_cameras: bool,
    /// Whether to load lights from the FBX file
    pub load_lights: bool,
    /// Whether to include raw source data in the loaded asset
    pub include_source: bool,
    /// Whether to convert coordinate systems (e.g., Y-up to Z-up)
    pub convert_coordinates: bool,
}

impl Default for FbxLoaderSettings {
    fn default() -> Self {
        Self {
            load_meshes: RenderAssetUsages::default(),
            load_materials: RenderAssetUsages::default(),
            load_cameras: true,
            load_lights: true,
            include_source: false,
            convert_coordinates: false,
        }
    }
}

/// Loader implementation for FBX files.
///
/// This loader handles reading FBX files and converting them into Bevy assets,
/// including meshes, materials, animations, and scene hierarchies.
#[derive(Default, bevy::reflect::TypePath)]
pub struct FbxLoader;

impl AssetLoader for FbxLoader {
    type Asset = Fbx;
    type Settings = FbxLoaderSettings;
    type Error = FbxError;

    async fn load(
        &self,
        reader: &mut dyn Reader,
        settings: &Self::Settings,
        load_context: &mut LoadContext<'_>,
    ) -> Result<Fbx, FbxError> {
        // Read file
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes).await?;

        // Basic validation
        if bytes.is_empty() {
            return Err(FbxError::InvalidData("Empty FBX file".to_string()));
        }
        if bytes.len() < 32 {
            return Err(FbxError::InvalidData("FBX file too small".to_string()));
        }

        let filename_hint = load_context.path().path().to_string_lossy().to_string();
        let root = parse_fbx(&bytes, &filename_hint)?;
        let texture_report = external_texture_path_report(&root);
        if texture_report.paths.is_empty() {
            if !texture_report.errors.is_empty() {
                warn_texture_failures(
                    load_context.path(),
                    texture_report.errors.iter().map(ToString::to_string),
                );
            }
            return process_scene(&root, settings, load_context, &HashMap::new());
        }

        // `ufbx::Scene` is not Send, so finish discovery and drop it before awaiting asset I/O.
        drop(root);
        let mut failures = texture_report
            .errors
            .iter()
            .map(ToString::to_string)
            .collect::<BTreeSet<_>>();
        let mut external_texture_bytes = HashMap::new();
        let asset_path = load_context.path().clone();
        for relative_path in texture_report.paths {
            let resolved_path = match asset_path.resolve_embed_str(&relative_path) {
                Ok(path) => path,
                Err(error) => {
                    failures.insert(format!("{relative_path} ({error})"));
                    continue;
                }
            };
            match load_context.read_asset_bytes(resolved_path.clone()).await {
                Ok(bytes) => {
                    external_texture_bytes.insert(relative_path, bytes);
                }
                Err(error) => {
                    failures.insert(format!("{resolved_path} ({error})"));
                }
            }
        }
        if !failures.is_empty() {
            warn_texture_failures(load_context.path(), failures);
        }

        let root = parse_fbx(&bytes, &filename_hint)?;
        process_scene(&root, settings, load_context, &external_texture_bytes)
    }

    fn extensions(&self) -> &[&str] {
        &["fbx"]
    }
}

fn parse_fbx(bytes: &[u8], filename_hint: &str) -> Result<ufbx::SceneRoot, FbxError> {
    ufbx::load_memory(
        bytes,
        ufbx::LoadOpts {
            target_unit_meters: 1.0,
            target_axes: ufbx::CoordinateAxes::right_handed_y_up(),
            filename: ufbx::StringOpt::Ref(filename_hint),
            ..Default::default()
        },
    )
    .map_err(|error| FbxError::UfbxError(format!("{error:?}")))
}

fn process_scene(
    scene: &ufbx::Scene,
    settings: &FbxLoaderSettings,
    load_context: &mut LoadContext<'_>,
    external_texture_bytes: &HashMap<String, Vec<u8>>,
) -> Result<Fbx, FbxError> {
    let (meshes, named_meshes, mesh_transforms, mesh_material_info) =
        process_meshes(scene, settings, load_context)?;
    let (materials, named_materials) = if !settings.load_materials.is_empty() {
        process_materials(scene, settings, load_context, external_texture_bytes)?
    } else {
        (Vec::new(), HashMap::new())
    };
    let (nodes, named_nodes, node_map) = process_nodes(scene, &meshes, load_context)?;
    let (skins, named_skins) = process_skins(scene, &node_map, load_context)?;
    let scene_handle = build_scene(
        scene,
        &meshes,
        &materials,
        &named_materials,
        &mesh_transforms,
        &mesh_material_info,
        settings,
        load_context,
    )?;

    Ok(Fbx {
        scenes: vec![scene_handle.clone()],
        named_scenes: HashMap::new(),
        meshes,
        named_meshes,
        materials,
        named_materials,
        nodes,
        named_nodes,
        skins,
        named_skins,
        default_scene: Some(scene_handle),
        axis_system: FbxAxisSystem {
            up: Vec3::Y,
            front: Vec3::Z,
            handedness: Handedness::Right,
        },
        unit_scale: 1.0,
        metadata: FbxMeta::default(),
    })
}

fn warn_texture_failures(
    asset_path: &bevy::asset::AssetPath<'_>,
    failures: impl IntoIterator<Item = String>,
) {
    let failures = failures.into_iter().collect::<Vec<_>>();
    warn!(
        "bevy_ufbx: skipped {} unavailable external texture reference(s) for {}: {}",
        failures.len(),
        asset_path,
        failures.join("; ")
    );
}
