//! Blocking dependency injection container.
//!
//! # Examples
//! ```
//! use std::collections::HashMap;
//! use std::error::Error;
//!
//! use syrette::{injectable, DIContainer};
//!
//! trait IDatabaseService
//! {
//!     fn get_all_records(&self, table_name: String) -> HashMap<String, String>;
//! }
//!
//! struct DatabaseService {}
//!
//! #[injectable(IDatabaseService)]
//! impl DatabaseService
//! {
//!     fn new() -> Self
//!     {
//!         Self {}
//!     }
//! }
//!
//! impl IDatabaseService for DatabaseService
//! {
//!     fn get_all_records(&self, table_name: String) -> HashMap<String, String>
//!     {
//!         // Do stuff here
//!         HashMap::<String, String>::new()
//!     }
//! }
//!
//! fn main() -> Result<(), Box<dyn Error>>
//! {
//!     let mut di_container = DIContainer::new();
//!
//!     di_container
//!         .bind::<dyn IDatabaseService>()
//!         .to::<DatabaseService>()
//!         .map_err(|err| err.to_string())?;
//!
//!     let database_service = di_container
//!         .get::<dyn IDatabaseService>()
//!         .map_err(|err| err.to_string())?
//!         .transient()?;
//!
//!     Ok(())
//! }
//! ```
use std::any::type_name;
use std::cell::RefCell;

use crate::di_container::binding_storage::DIContainerBindingStorage;
use crate::di_container::blocking::binding::builder::BindingBuilder;
use crate::di_container::BindingOptions;
use crate::errors::di_container::DIContainerError;
use crate::private::cast::boxed::CastBox;
use crate::private::cast::rc::CastRc;
use crate::provider::blocking::{IProvider, Providable};
use crate::ptr::SomePtr;
use crate::util::use_double;

use_double!(crate::dependency_history::DependencyHistory);

pub mod binding;

#[cfg(not(test))]
pub(crate) type BindingOptionsWithLt<'a> = BindingOptions<'a>;

#[cfg(test)]
pub(crate) type BindingOptionsWithLt = BindingOptions<'static>;

/// Blocking dependency injection container.
#[derive(Default)]
pub struct DIContainer
{
    binding_storage: RefCell<DIContainerBindingStorage<dyn IProvider<Self>>>,
}

impl DIContainer
{
    /// Returns a new `DIContainer`.
    #[must_use]
    pub fn new() -> Self
    {
        Self {
            binding_storage: RefCell::new(DIContainerBindingStorage::new()),
        }
    }
}

#[cfg_attr(test, mockall::automock)]
impl DIContainer
{
    /// Returns a new [`BindingBuilder`] for the given interface.
    ///
    /// # Examples
    /// ```
    /// # use syrette::{DIContainer, injectable};
    /// #
    /// # struct DiskWriter {}
    /// #
    /// # #[injectable]
    /// # impl DiskWriter
    /// # {
    /// #     fn new() -> Self
    /// #     {
    /// #         Self {}
    /// #     }
    /// # }
    /// #
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let mut di_container = DIContainer::new();
    ///
    /// di_container.bind::<DiskWriter>().to::<DiskWriter>()?;
    /// #
    /// # Ok(())
    /// # }
    /// ```
    #[allow(clippy::missing_panics_doc)]
    pub fn bind<Interface>(&mut self) -> BindingBuilder<'_, Interface>
    where
        Interface: 'static + ?Sized,
    {
        #[cfg(test)]
        panic!("Nope");

        #[cfg(not(test))]
        BindingBuilder::new(self, DependencyHistory::new)
    }

    /// Returns the type bound with `Interface`.
    ///
    /// # Errors
    /// Will return `Err` if:
    /// - No binding for `Interface` exists
    /// - Resolving the binding for `Interface` fails
    /// - Casting the binding for `Interface` fails
    ///
    /// # Examples
    /// ```
    /// # use syrette::{DIContainer, injectable};
    /// #
    /// # struct DeviceManager {}
    /// #
    /// # #[injectable]
    /// # impl DeviceManager
    /// # {
    /// #     fn new() -> Self
    /// #     {
    /// #         Self {}
    /// #     }
    /// # }
    /// #
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let mut di_container = DIContainer::new();
    ///
    /// di_container.bind::<DeviceManager>().to::<DeviceManager>()?;
    ///
    /// let device_manager = di_container.get::<DeviceManager>()?.transient();
    /// #
    /// # Ok(())
    /// # }
    /// ```
    pub fn get<Interface>(&self) -> Result<SomePtr<Interface>, DIContainerError>
    where
        Interface: 'static + ?Sized,
    {
        self.get_bound::<Interface>(DependencyHistory::new(), BindingOptions::new())
    }

    /// Returns the type bound with `Interface` and the specified name.
    ///
    /// # Errors
    /// Will return `Err` if:
    /// - No binding for `Interface` with name `name` exists
    /// - Resolving the binding for `Interface` fails
    /// - Casting the binding for `Interface` fails
    ///
    /// # Examples
    /// ```
    /// # use syrette::{DIContainer, injectable};
    /// #
    /// # struct DeviceManager {}
    /// #
    /// # #[injectable]
    /// # impl DeviceManager
    /// # {
    /// #     fn new() -> Self
    /// #     {
    /// #         Self {}
    /// #     }
    /// # }
    /// #
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let mut di_container = DIContainer::new();
    ///
    /// di_container
    ///     .bind::<DeviceManager>()
    ///     .to::<DeviceManager>()?
    ///     .in_transient_scope()
    ///     .when_named("usb")?;
    ///
    /// let device_manager = di_container.get_named::<DeviceManager>("usb")?.transient();
    /// #
    /// # Ok(())
    /// # }
    /// ```
    pub fn get_named<Interface>(
        &self,
        name: &'static str,
    ) -> Result<SomePtr<Interface>, DIContainerError>
    where
        Interface: 'static + ?Sized,
    {
        self.get_bound::<Interface>(
            DependencyHistory::new(),
            BindingOptions::new().name(name),
        )
    }

    /// Returns the type bound with `Interface` where the binding has the specified
    /// options.
    ///
    /// `dependency_history` is passed to the bound type when it is being resolved.
    ///
    /// # Errors
    /// Will return `Err` if:
    /// - No binding for `Interface` exists
    /// - Resolving the binding for `Interface` fails
    /// - Casting the binding for `Interface` fails
    ///
    /// # Examples
    /// ```no_run
    /// # use syrette::di_container::blocking::DIContainer;
    /// # use syrette::dependency_history::DependencyHistory;
    /// # use syrette::di_container::BindingOptions;
    /// #
    /// # struct EventHandler {}
    /// # struct Button {}
    /// #
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// # let di_container = DIContainer::new();
    /// #
    /// let mut dependency_history = DependencyHistory::new();
    ///
    /// dependency_history.push::<EventHandler>();
    ///
    /// di_container.get_bound::<Button>(
    ///     dependency_history,
    ///     BindingOptions::new().name("huge_red"),
    /// )?;
    /// #
    /// # Ok(())
    /// # }
    /// ```
    pub fn get_bound<Interface>(
        &self,
        dependency_history: DependencyHistory,
        binding_options: BindingOptionsWithLt,
    ) -> Result<SomePtr<Interface>, DIContainerError>
    where
        Interface: 'static + ?Sized,
    {
        let binding_providable = self
            .get_binding_providable::<Interface>(binding_options, dependency_history)?;

        match binding_providable {
            Providable::Transient(transient_binding) => Ok(SomePtr::Transient(
                transient_binding.cast::<Interface>().map_err(|_| {
                    DIContainerError::CastFailed {
                        interface: type_name::<Interface>(),
                        binding_kind: "transient",
                    }
                })?,
            )),
            Providable::Singleton(singleton_binding) => Ok(SomePtr::Singleton(
                singleton_binding.cast::<Interface>().map_err(|_| {
                    DIContainerError::CastFailed {
                        interface: type_name::<Interface>(),
                        binding_kind: "singleton",
                    }
                })?,
            )),
            #[cfg(feature = "factory")]
            Providable::Factory(factory_binding) => {
                use crate::private::factory::IFactory;

                let factory = factory_binding
                    .cast::<dyn IFactory<Interface, Self>>()
                    .map_err(|_| DIContainerError::CastFailed {
                        interface: type_name::<Interface>(),
                        binding_kind: "factory",
                    })?;

                Ok(SomePtr::Factory(factory.call(self).into()))
            }
            #[cfg(feature = "factory")]
            Providable::DefaultFactory(factory_binding) => {
                use crate::private::factory::IFactory;
                use crate::ptr::TransientPtr;

                type DefaultFactoryFn<Interface> = dyn IFactory<
                    dyn Fn<(), Output = TransientPtr<Interface>>,
                    DIContainer,
                >;

                let default_factory = factory_binding
                    .cast::<DefaultFactoryFn<Interface>>()
                    .map_err(|_| DIContainerError::CastFailed {
                        interface: type_name::<Interface>(),
                        binding_kind: "default factory",
                    })?;

                Ok(SomePtr::Transient(default_factory.call(self)()))
            }
        }
    }

    fn has_binding<Interface>(&self, binding_options: BindingOptionsWithLt) -> bool
    where
        Interface: ?Sized + 'static,
    {
        self.binding_storage
            .borrow()
            .has::<Interface>(binding_options)
    }

    fn set_binding<Interface>(
        &self,
        binding_options: BindingOptions<'static>,
        provider: Box<dyn IProvider<Self>>,
    ) where
        Interface: 'static + ?Sized,
    {
        self.binding_storage
            .borrow_mut()
            .set::<Interface>(binding_options, provider);
    }

    fn remove_binding<Interface>(
        &self,
        binding_options: BindingOptions<'static>,
    ) -> Option<Box<dyn IProvider<Self>>>
    where
        Interface: 'static + ?Sized,
    {
        self.binding_storage
            .borrow_mut()
            .remove::<Interface>(binding_options)
    }
}

impl DIContainer
{
    fn get_binding_providable<Interface>(
        &self,
        binding_options: BindingOptionsWithLt,
        dependency_history: DependencyHistory,
    ) -> Result<Providable<Self>, DIContainerError>
    where
        Interface: 'static + ?Sized,
    {
        let name = binding_options.name;

        self.binding_storage
            .borrow()
            .get::<Interface>(binding_options)
            .map_or_else(
                || {
                    Err(DIContainerError::BindingNotFound {
                        interface: type_name::<Interface>(),
                        name: name.as_ref().map(ToString::to_string),
                    })
                },
                Ok,
            )?
            .provide(self, dependency_history)
            .map_err(|err| DIContainerError::BindingResolveFailed {
                reason: err,
                interface: type_name::<Interface>(),
            })
    }
}

#[cfg(test)]
mod tests
{
    use super::*;
    use crate::provider::blocking::MockIProvider;
    use crate::ptr::{SingletonPtr, TransientPtr};
    use crate::test_utils::subjects;

    #[test]
    fn can_get()
    {
        let di_container = DIContainer::new();

        let mut mock_provider = MockIProvider::new();

        mock_provider.expect_provide().returning(|_, _| {
            Ok(Providable::Transient(TransientPtr::new(
                subjects::UserManager::new(),
            )))
        });

        di_container
            .binding_storage
            .borrow_mut()
            .set::<dyn subjects::IUserManager>(
                BindingOptions::new(),
                Box::new(mock_provider),
            );

        di_container
            .get::<dyn subjects::IUserManager>()
            .unwrap()
            .transient()
            .unwrap();
    }

    #[test]
    fn can_get_named()
    {
        let di_container = DIContainer::new();

        let mut mock_provider = MockIProvider::new();

        mock_provider.expect_provide().returning(|_, _| {
            Ok(Providable::Transient(TransientPtr::new(
                subjects::UserManager::new(),
            )))
        });

        di_container
            .binding_storage
            .borrow_mut()
            .set::<dyn subjects::IUserManager>(
                BindingOptions::new().name("special"),
                Box::new(mock_provider),
            );

        di_container
            .get_named::<dyn subjects::IUserManager>("special")
            .unwrap()
            .transient()
            .unwrap();
    }

    #[test]
    fn can_get_singleton()
    {
        let di_container = DIContainer::new();

        let mut mock_provider = MockIProvider::new();

        let mut singleton = SingletonPtr::new(subjects::Number::new());

        SingletonPtr::get_mut(&mut singleton).unwrap().num = 2820;

        mock_provider
            .expect_provide()
            .returning_st(move |_, _| Ok(Providable::Singleton(singleton.clone())));

        di_container
            .binding_storage
            .borrow_mut()
            .set::<dyn subjects::INumber>(BindingOptions::new(), Box::new(mock_provider));

        let first_number_rc = di_container
            .get::<dyn subjects::INumber>()
            .unwrap()
            .singleton()
            .unwrap();

        assert_eq!(first_number_rc.get(), 2820);

        let second_number_rc = di_container
            .get::<dyn subjects::INumber>()
            .unwrap()
            .singleton()
            .unwrap();

        assert_eq!(first_number_rc.as_ref(), second_number_rc.as_ref());
    }

    #[test]
    fn can_get_singleton_named()
    {
        let di_container = DIContainer::new();

        let mut mock_provider = MockIProvider::new();

        let mut singleton = SingletonPtr::new(subjects::Number::new());

        SingletonPtr::get_mut(&mut singleton).unwrap().num = 2820;

        mock_provider
            .expect_provide()
            .returning_st(move |_, _| Ok(Providable::Singleton(singleton.clone())));

        di_container
            .binding_storage
            .borrow_mut()
            .set::<dyn subjects::INumber>(
                BindingOptions::new().name("cool"),
                Box::new(mock_provider),
            );

        let first_number_rc = di_container
            .get_named::<dyn subjects::INumber>("cool")
            .unwrap()
            .singleton()
            .unwrap();

        assert_eq!(first_number_rc.get(), 2820);

        let second_number_rc = di_container
            .get_named::<dyn subjects::INumber>("cool")
            .unwrap()
            .singleton()
            .unwrap();

        assert_eq!(first_number_rc.as_ref(), second_number_rc.as_ref());
    }

    #[test]
    #[cfg(feature = "factory")]
    fn can_get_factory()
    {
        use crate::private::castable_factory::CastableFactory;
        use crate::ptr::FactoryPtr;

        trait IUserManager
        {
            fn add_user(&mut self, user_id: i128);

            fn remove_user(&mut self, user_id: i128);
        }

        struct UserManager
        {
            users: Vec<i128>,
        }

        impl UserManager
        {
            fn new(users: Vec<i128>) -> Self
            {
                Self { users }
            }
        }

        impl IUserManager for UserManager
        {
            fn add_user(&mut self, user_id: i128)
            {
                self.users.push(user_id);
            }

            fn remove_user(&mut self, user_id: i128)
            {
                let user_index =
                    self.users.iter().position(|user| *user == user_id).unwrap();

                self.users.remove(user_index);
            }
        }

        use crate as syrette;

        #[crate::factory]
        type IUserManagerFactory = dyn Fn(Vec<i128>) -> TransientPtr<dyn IUserManager>;

        let di_container = DIContainer::new();

        let factory_func: &dyn Fn(&DIContainer) -> Box<IUserManagerFactory> = &|_| {
            Box::new(move |users| {
                let user_manager: TransientPtr<dyn IUserManager> =
                    TransientPtr::new(UserManager::new(users));

                user_manager
            })
        };

        let mut mock_provider = MockIProvider::new();

        mock_provider.expect_provide().returning_st(|_, _| {
            Ok(Providable::Factory(FactoryPtr::new(CastableFactory::new(
                factory_func,
            ))))
        });

        di_container
            .binding_storage
            .borrow_mut()
            .set::<IUserManagerFactory>(BindingOptions::new(), Box::new(mock_provider));

        di_container
            .get::<IUserManagerFactory>()
            .unwrap()
            .factory()
            .unwrap();
    }

    #[test]
    #[cfg(feature = "factory")]
    fn can_get_factory_named()
    {
        use crate::private::castable_factory::CastableFactory;
        use crate::ptr::FactoryPtr;

        trait IUserManager
        {
            fn add_user(&mut self, user_id: i128);

            fn remove_user(&mut self, user_id: i128);
        }

        struct UserManager
        {
            users: Vec<i128>,
        }

        impl UserManager
        {
            fn new(users: Vec<i128>) -> Self
            {
                Self { users }
            }
        }

        impl IUserManager for UserManager
        {
            fn add_user(&mut self, user_id: i128)
            {
                self.users.push(user_id);
            }

            fn remove_user(&mut self, user_id: i128)
            {
                let user_index =
                    self.users.iter().position(|user| *user == user_id).unwrap();

                self.users.remove(user_index);
            }
        }

        use crate as syrette;

        #[crate::factory]
        type IUserManagerFactory = dyn Fn(Vec<i128>) -> TransientPtr<dyn IUserManager>;

        let di_container = DIContainer::new();

        let factory_func: &dyn Fn(&DIContainer) -> Box<IUserManagerFactory> = &|_| {
            Box::new(move |users| {
                let user_manager: TransientPtr<dyn IUserManager> =
                    TransientPtr::new(UserManager::new(users));

                user_manager
            })
        };

        let mut mock_provider = MockIProvider::new();

        mock_provider.expect_provide().returning_st(|_, _| {
            Ok(Providable::Factory(FactoryPtr::new(CastableFactory::new(
                factory_func,
            ))))
        });

        di_container
            .binding_storage
            .borrow_mut()
            .set::<IUserManagerFactory>(
                BindingOptions::new().name("special"),
                Box::new(mock_provider),
            );

        di_container
            .get_named::<IUserManagerFactory>("special")
            .unwrap()
            .factory()
            .unwrap();
    }

    #[test]
    fn has_binding_works()
    {
        let di_container = DIContainer::new();

        // No binding is present yet
        assert!(!di_container.has_binding::<subjects::Ninja>(BindingOptions::new()));

        di_container
            .binding_storage
            .borrow_mut()
            .set::<subjects::Ninja>(
                BindingOptions::new(),
                Box::new(MockIProvider::new()),
            );

        assert!(di_container.has_binding::<subjects::Ninja>(BindingOptions::new()));
    }

    #[test]
    fn set_binding_works()
    {
        let di_container = DIContainer::new();

        di_container.set_binding::<subjects::Ninja>(
            BindingOptions::new(),
            Box::new(MockIProvider::new()),
        );

        assert!(di_container
            .binding_storage
            .borrow_mut()
            .has::<subjects::Ninja>(BindingOptions::new()));
    }

    #[test]
    fn remove_binding_works()
    {
        let di_container = DIContainer::new();

        di_container
            .binding_storage
            .borrow_mut()
            .set::<subjects::Ninja>(
                BindingOptions::new(),
                Box::new(MockIProvider::new()),
            );

        assert!(
            // Formatting is weird without this comment
            di_container
                .remove_binding::<subjects::Ninja>(BindingOptions::new())
                .is_some()
        );

        assert!(
            // Formatting is weird without this comment
            !di_container
                .binding_storage
                .borrow_mut()
                .has::<subjects::Ninja>(BindingOptions::new())
        );
    }
}