pub use noema_macros::{collection, item};
use std::fmt::Display;
use std::sync::Arc;

use crate::core::Container;

/// Trait for generate a Collecion Types
///
/// # Examples
/// ```ignore
/// #[collection(i32, name = "Steps")]
/// struct Steps;
/// ```
pub trait Collection {
    /// Define the valur for all items
    type Value: Display + Eq + Default;

    /// Define a name for collection
    fn name(&self) -> String;
    /// Define what is the collection
    fn describe(&self) -> String {
        format!("Collection with name: {}", self.name())
    }
    /// default value of collection (by defect is default value of Value)
    fn default_value(&self) -> Self::Value {
        <Self::Value as Default>::default()
    }
}

/// Item trait for items collections
///
/// # Examples
/// ```ignore
/// #[collection(i32, name = "Steps")]
/// struct Steps;
/// #[item(Steps, value = 1)]
/// struct StepOne;
///
/// let steps = items_collection_locator::<Steps>();
/// assert!(steps.collection().name() == "Steps".to_string());
/// ```
pub trait Item: Send + Sync {
    type CollectionType: Collection + Default;
    /// Get value of the flag
    fn value(&self) -> <Self::CollectionType as Collection>::Value;

    /// Get name of the flag
    fn name(&self) -> String;

    /// Get description of the flag
    fn description(&self) -> String {
        format!("Item {}: {}", self.name(), self.value())
    }

    /// Convert to tuple when (name, value, description)
    fn to_tuple(&self) -> (String, <Self::CollectionType as Collection>::Value, String) {
        (self.name(), self.value(), self.description())
    }
}

use std::sync::OnceLock;
pub trait ItemsCollectionLock<C: Collection + Default + ?Sized + Send + Sync> {
    fn collection(&self) -> &OnceLock<Arc<Vec<Arc<dyn Item<CollectionType = C> + Send + Sync>>>>;
    fn set_collection(
        &self,
        collection: Arc<Vec<Arc<dyn Item<CollectionType = C> + Send + Sync>>>,
    ) {
        if self.collection().get().is_some() {
            panic!(
                "Collection {} already set",
                <C as Default>::default().name()
            );
        }
        self.collection().set(collection).ok();
    }
    fn values(&self) -> Arc<Vec<Arc<dyn Item<CollectionType = C> + Send + Sync>>> {
        self.collection()
            .get()
            .expect(format!("Collection {} not set", <C as Default>::default().name()).as_str())
            .clone()
    }
}

/// Collection for Items of type I
pub trait ItemsCollection<C: Collection + Default + ?Sized + Send + Sync> {
    /// Get all flag elements
    fn all(&self) -> Arc<Vec<Arc<dyn Item<CollectionType = C> + Send + Sync>>>;

    fn type_properties(&self) -> C {
        <C as Default>::default()
    }

    // Get elements matching a predicate
    // # Examples
    // ```
    // let name = "Hola".to_string();
    // let new_items = items_collection.find(&|e| e.name() == &name)
    // ```
    fn find(
        &self,
        predicate: &dyn Fn(&Arc<dyn Item<CollectionType = C> + Send + Sync>) -> bool,
    ) -> Vec<Arc<dyn Item<CollectionType = C> + Send + Sync>> {
        let mut result = Vec::new();
        for element in self.all().iter() {
            if predicate(element) {
                result.push(element.clone());
            }
        }
        result
    }

    /// Get one enumeration element by value
    fn by_value(&self, value: C::Value) -> Option<Arc<dyn Item<CollectionType = C> + Send + Sync>> {
        self.find(&|e| e.value() == value).into_iter().next()
    }

    /// Get one enumeration element by name
    fn by_name(&self, name: &str) -> Option<Arc<dyn Item<CollectionType = C> + Send + Sync>> {
        self.find(&|e| e.name() == name).into_iter().next()
    }

    /// Convert to tuple when (name, value, description)
    fn to_tuple(&self) -> Vec<(String, C::Value, String)> {
        let mut result = Vec::new();
        for e in self.all().iter() {
            result.push(e.to_tuple());
        }
        result
    }
}

/// Get items collection for item type with collection type C
/// # Examples
/// ```ignore
/// let steps = items_collection::<Steps>();
/// assert!(steps.type_properties().name() == "Steps".to_string());
/// ```
pub fn items_collection<C: Send + Sync>() -> impl ItemsCollection<C> + Send + Sync
where
    C: Collection + Default + 'static,
    Container: ItemsCollection<C> + Send + Sync,
{
    return Container;
}

/// Collection Builder
pub struct CollectionBuilder<C: ?Sized> {
    items: Vec<Arc<dyn Item<CollectionType = C> + Send + Sync>>,
}

impl<C: ?Sized + Collection + Send + Sync + Default> CollectionBuilder<C> {
    pub fn new() -> Self {
        Self { items: vec![] }
    }

    pub fn add<I>(mut self) -> Self
    where
        I: Item<CollectionType = C> + Send + Sync + 'static + Default,
    {
        self.items.push(Arc::new(I::default()));
        self
    }

    pub fn build(self)
    where
        Container: ItemsCollectionLock<C>,
    {
        Container.set_collection(Arc::new(self.items));
    }
}

#[cfg(test)]
mod test {
    use crate::items::CollectionBuilder;

    #[test]
    fn test_items_collections() {
        println!("Testing items collections...");
        use crate::core::*;
        use crate::items::{Collection, Item, ItemsCollection, ItemsCollectionLock};
        use noema_macros::{collection, item};

        #[collection(i32, name = "Steps")]
        struct Steps;

        #[item(Steps, value = 12)]
        struct StepOne;

        #[item(Steps, value = 14)]
        struct StepTwo;

        #[item(Steps, value = 16)]
        struct StepThree;
        println!("Setting collection...");

        CollectionBuilder::<Steps>::new()
            .add::<StepOne>()
            .add::<StepTwo>()
            .add::<StepThree>()
            .build();

        #[derive(Injectable)]
        struct TestPapu {
            t: Arc<dyn ItemsCollection<Steps> + Send + Sync>,
        }

        let steps = TestPapu::inject(&Container).t;
        println!("Hola");
        println!("{:?}", steps.type_properties().name());
        println!("{:?}", steps.to_tuple());
        assert!(steps.type_properties().name() == "Steps".to_string());
    }
}
