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

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

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

/// 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 any renderable surface.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MaterialSlot {
    pub id: ComponentInstanceId,
    pub name: String,
    /// Actor-owned shared Material or Material Instance assignment.
    /// Model defaults and imported source materials remain asset-owned.
    #[serde(default)]
    pub material: Option<MaterialRef>,
}

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

    pub fn hydrated_selection(&self) -> HydratedMaterialSelection {
        self.material
            .clone()
            .map(|reference| HydratedMaterialSelection::Project {
                reference,
                layer: HydratedMaterialLayer::Actor,
            })
            .unwrap_or_default()
    }
}

/// 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 primitive, static, and skinned authoring components.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MaterialSlotSet {
    #[serde(default)]
    pub slots: Vec<MaterialSlot>,
    #[serde(default)]
    pub orphaned_assignments: Vec<OrphanedMaterialAssignment>,
}

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

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

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

/// Source-compatible name retained for the schema-v4 transition.
pub type RendererMaterialSlot = MaterialSlot;

/// Source-compatible name retained for the schema-v4 transition.
pub type RendererMaterialSet = MaterialSlotSet;

/// 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,
    pub parameters: Vec<MaterialParameter>,
    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(Reflect, Debug, Clone, Copy, PartialEq, Eq)]
#[reflect(Debug, PartialEq)]
pub enum HydratedMaterialLayer {
    Actor,
    Model,
}

#[derive(Reflect, Debug, Clone, PartialEq, Eq, Default)]
#[reflect(Debug, PartialEq, Default)]
pub enum HydratedMaterialSelection {
    Project {
        reference: MaterialRef,
        layer: HydratedMaterialLayer,
    },
    ImportedSource {
        reference: MaterialRef,
    },
    #[default]
    Inherit,
}

impl HydratedMaterialSelection {
    pub fn project_reference(&self) -> Option<&MaterialRef> {
        match self {
            Self::Project { reference, .. } => Some(reference),
            Self::ImportedSource { .. } | Self::Inherit => None,
        }
    }
}

#[derive(Component, Reflect, Debug, Clone, PartialEq, Eq)]
#[reflect(Component, Debug, PartialEq)]
pub struct HydratedMaterialSlotBinding {
    pub owner: Entity,
    pub slot_id: ComponentInstanceId,
    pub selection: HydratedMaterialSelection,
}

/// Source-compatible name retained for the schema-v4 transition.
pub type HydratedRendererMaterialBinding = HydratedMaterialSlotBinding;

/// Runtime ownership marker consumed by the project terrain-layer renderer.
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq)]
pub struct HydratedTerrainMaterialBinding {
    pub owner: Entity,
    pub layers: Vec<TerrainMaterialLayer>,
}

impl Default for HydratedTerrainMaterialBinding {
    fn default() -> Self {
        Self {
            owner: Entity::PLACEHOLDER,
            layers: Vec::new(),
        }
    }
}

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

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

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

        assert_eq!(slot.effective_material(), Some(&assigned));
        assert!(matches!(
            slot.hydrated_selection(),
            HydratedMaterialSelection::Project {
                layer: HydratedMaterialLayer::Actor,
                ..
            }
        ));
        slot.material = None;
        assert_eq!(slot.effective_material(), None);
        assert_eq!(
            slot.hydrated_selection(),
            HydratedMaterialSelection::Inherit
        );
    }

    #[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(),
                material: None,
            }],
            orphaned_assignments: vec![],
        };

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