//! [`DisabledComponents`]: a hidden per-entity set of disabled component names.
//!
//! Some game-objects need a component *attached but not active* — e.g. a
//! camera that defaults disabled and a script turns it on at a trigger. ECS
//! component-sets don't carry an "active" bit per component on their own, so
//! this component stores the set of *type names* (matching the reflection
//! registry) that should be skipped by systems on this entity.
//!
//! - Each engine system that runs on a per-entity component query consults
//!   [`Scene::is_component_disabled`](crate::scene::Scene::is_component_disabled)
//!   (or this component directly) before acting; it's the Unity
//!   "Component.enabled" equivalent in an archetypal ECS.
//! - The editor inspector reads + writes it through a per-component
//!   checkbox, hides the component itself from view (it's metadata, not
//!   authored data), and copies the set on Duplicate.

use std::collections::HashSet;

use serde::{Deserialize, Serialize};

/// Per-entity set of *disabled* component type names.
///
/// Names match the reflection registry (e.g. `"MeshRenderer"`). An absent
/// component (or an empty set) means every component on the entity is active.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DisabledComponents {
    /// Disabled component type names. Stored as `String` so the data
    /// round-trips through RON without the `&'static str` reference issue.
    pub disabled: HashSet<String>,
}

impl DisabledComponents {
    /// An empty set — every component is active.
    pub fn new() -> Self {
        Self::default()
    }

    /// Whether the component with this type name is disabled.
    pub fn is_disabled(&self, type_name: &str) -> bool {
        self.disabled.contains(type_name)
    }

    /// Marks the component disabled (`true`) or active (`false`). Adds or
    /// removes the entry as needed.
    pub fn set_disabled(&mut self, type_name: &str, disabled: bool) {
        if disabled {
            self.disabled.insert(type_name.to_string());
        } else {
            self.disabled.remove(type_name);
        }
    }

    /// True if no components are currently disabled — a hint to systems that
    /// the entire `DisabledComponents` component can be removed.
    pub fn is_empty(&self) -> bool {
        self.disabled.is_empty()
    }
}

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

    #[test]
    fn default_is_empty_and_disables_nothing() {
        let d = DisabledComponents::new();
        assert!(d.is_empty());
        assert!(!d.is_disabled("MeshRenderer"));
    }

    #[test]
    fn set_disabled_toggles_membership() {
        let mut d = DisabledComponents::new();
        d.set_disabled("MeshRenderer", true);
        assert!(d.is_disabled("MeshRenderer"));
        assert!(!d.is_empty());
        // Idempotent.
        d.set_disabled("MeshRenderer", true);
        assert_eq!(d.disabled.len(), 1);
        // Re-enable removes the entry.
        d.set_disabled("MeshRenderer", false);
        assert!(!d.is_disabled("MeshRenderer"));
        assert!(d.is_empty());
    }

    #[test]
    fn round_trips_through_ron() {
        let mut d = DisabledComponents::new();
        d.set_disabled("MeshRenderer", true);
        d.set_disabled("RigidBody", true);
        let text = ron::to_string(&d).unwrap();
        let back: DisabledComponents = ron::from_str(&text).unwrap();
        assert_eq!(back, d);
    }
}
