//! Shared material references and renderer-slot authoring contracts.

use bevy::prelude::*;
use serde::{Deserialize, Serialize};

use crate::{ComponentInstanceId, EditorAssetRef, MaterialParameter, MaterialTextureBinding};

/// A stable reference to a project material, material instance, or imported material subasset.
///
/// The registry UUID/subasset ID are authoritative. `EditorAssetRef::source_path` is only a
/// load-time cache and may be repaired when an asset moves.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MaterialRef(pub EditorAssetRef);

impl MaterialRef {
    pub fn new(asset: EditorAssetRef) -> Self {
        Self(asset)
    }

    pub fn is_resolved(&self) -> bool {
        self.0.is_resolved()
    }
}

impl From<EditorAssetRef> for MaterialRef {
    fn from(value: EditorAssetRef) -> Self {
        Self(value)
    }
}

/// One stable, named material assignment exposed by a renderer.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct RendererMaterialSlot {
    pub id: ComponentInstanceId,
    pub name: String,
    /// Read-only imported default. Clearing `material` returns to this reference.
    #[serde(default)]
    pub source_material: Option<MaterialRef>,
    /// Explicit shared Material or Material Instance assignment.
    #[serde(default)]
    pub material: Option<MaterialRef>,
}

impl RendererMaterialSlot {
    pub fn effective_material(&self) -> Option<&MaterialRef> {
        self.material.as_ref().or(self.source_material.as_ref())
    }
}

/// Assignment retained after a source reimport removes or renames its draw slot.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct OrphanedMaterialAssignment {
    pub slot_id: ComponentInstanceId,
    #[serde(default)]
    pub last_known_name: String,
    pub material: MaterialRef,
}

/// Material slots shared by static and skinned renderer authoring components.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct RendererMaterialSet {
    #[serde(default)]
    pub slots: Vec<RendererMaterialSlot>,
    #[serde(default)]
    pub orphaned_assignments: Vec<OrphanedMaterialAssignment>,
}

impl RendererMaterialSet {
    pub fn slot(&self, id: &ComponentInstanceId) -> Option<&RendererMaterialSlot> {
        self.slots.iter().find(|slot| &slot.id == id)
    }

    pub fn slot_mut(&mut self, id: &ComponentInstanceId) -> Option<&mut RendererMaterialSlot> {
        self.slots.iter_mut().find(|slot| &slot.id == id)
    }

    pub fn effective_material(&self, id: &ComponentInstanceId) -> Option<&MaterialRef> {
        self.slot(id)
            .and_then(RendererMaterialSlot::effective_material)
    }
}

/// Runtime-only property overrides analogous to a material property block.
///
/// Blocks never alter shared assets and are deliberately excluded from scene/prefab persistence.
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[reflect(Default, Debug, PartialEq)]
pub struct MaterialPropertyBlock {
    pub slot_id: ComponentInstanceId,
    #[reflect(ignore)]
    pub parameters: Vec<MaterialParameter>,
    #[reflect(ignore)]
    pub textures: Vec<MaterialTextureBinding>,
}

#[derive(Component, Reflect, Default, Debug, Clone, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq)]
pub struct MaterialPropertyBlocks {
    pub slots: Vec<MaterialPropertyBlock>,
}

/// Runtime ownership/binding marker placed on every hydrated renderer draw entity.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component, Debug, PartialEq)]
pub struct HydratedRendererMaterialBinding {
    pub owner: Entity,
    pub slot_id: ComponentInstanceId,
    pub effective_material: Option<MaterialRef>,
}

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

    fn material_ref(id: &str) -> MaterialRef {
        MaterialRef::new(EditorAssetRef::new(id, "material:source", id))
    }

    #[test]
    fn explicit_assignment_precedes_imported_source() {
        let source = material_ref("source");
        let assigned = material_ref("assigned");
        let mut slot = RendererMaterialSlot {
            id: ComponentInstanceId::new("slot:body"),
            name: "Body".into(),
            source_material: Some(source.clone()),
            material: Some(assigned.clone()),
        };

        assert_eq!(slot.effective_material(), Some(&assigned));
        slot.material = None;
        assert_eq!(slot.effective_material(), Some(&source));
    }

    #[test]
    fn missing_slot_does_not_fall_back_by_display_name() {
        let set = RendererMaterialSet {
            slots: vec![RendererMaterialSlot {
                id: ComponentInstanceId::new("slot:body"),
                name: "Body".into(),
                source_material: Some(material_ref("source")),
                material: None,
            }],
            orphaned_assignments: vec![],
        };

        assert!(set
            .effective_material(&ComponentInstanceId::new("slot:renamed"))
            .is_none());
    }
}
