//! Ditto Builder
//!
//! Provides idiomatic configuration of a Ditto instance using the "Builder
//! Pattern"

use ffi_sdk::Platform;

use super::*;
use crate::identity::Identity;

/// Builder for [`Ditto`]
pub struct DittoBuilder {
    ditto_root: Option<Arc<dyn DittoRoot>>,
    identity: Option<Arc<dyn Identity>>,
    minimum_log_level: LogLevel,
    transport_config: Option<TransportConfig>,
}

impl DittoBuilder {
    /// Create a new, empty builder for a [`Ditto`](crate::prelude::Ditto) instance.
    pub fn new() -> DittoBuilder {
        DittoBuilder {
            ditto_root: None,
            identity: None,
            minimum_log_level: LogLevel::Info,
            transport_config: None,
        }
    }

    /// Set the root directory where Ditto will store its data.
    pub fn with_root(mut self, ditto_root: Arc<dyn DittoRoot>) -> Self {
        self.ditto_root = Some(ditto_root);
        self
    }

    /// Configure the minimum log level for the [`Ditto`](crate::prelude::Ditto) instance.
    pub fn with_minimum_log_level(mut self, log_level: LogLevel) -> Self {
        self.minimum_log_level = log_level;
        self
    }

    /// Build a [`Ditto`](crate::prelude::Ditto) instance with a temporary storage directory which
    /// will be destroyed on exit.
    pub fn with_temp_dir(mut self) -> Self {
        let root = TempRoot::new();
        self.ditto_root = Some(Arc::new(root));
        self
    }

    fn platform() -> Platform {
        using!(match () {
            use ffi_sdk::Platform;
            | _case if cfg!(target_os = "windows") => Platform::Windows,
            | _case if cfg!(target_os = "android") => Platform::Android,
            | _case if cfg!(target_os = "macos") => Platform::Mac,
            | _case if cfg!(target_os = "ios") => Platform::Ios,
            | _case if cfg!(target_os = "linux") => Platform::Linux,
            | _default => Platform::Unknown,
        })
    }

    fn sdk_version() -> String {
        let sdk_semver = env!("CARGO_PKG_VERSION");
        sdk_semver.to_string()
    }

    fn init_sdk_version() {
        let platform = Self::platform();
        let sdk_semver = Self::sdk_version();
        let c_version = char_p::new(sdk_semver);
        unsafe {
            ffi_sdk::ditto_init_sdk_version(platform, ffi_sdk::Language::Rust, c_version.as_ref());
        }
    }

    fn init_logging(&self) {
        unsafe {
            ffi_sdk::ditto_logger_init();
            ffi_sdk::ditto_logger_minimum_log_level(self.minimum_log_level);
            ffi_sdk::ditto_logger_enabled(true);
        }
    }

    /// Provide a factory [`FnOnce`] which will create and configure the
    /// [`Identity`](crate::prelude::Identity) for the [`Ditto`](crate::prelude::Ditto) instance.
    pub fn with_identity<F, I>(mut self, factory: F) -> Result<Self, DittoError>
    where
        F: FnOnce(Arc<dyn DittoRoot>) -> Result<I, DittoError>,
        I: Identity + 'static, // must return something ownable
    {
        match &self.ditto_root {
            Some(root) => {
                let identity = factory(root.retain())?;
                self.identity = Some(Arc::new(identity));
            }
            None => {
                let msg = "A valid DittoRoot directory must be provided before configuring the \
                           Identity"
                    .to_string();
                return Err(DittoError::new(ErrorKind::Config, msg));
            }
        };
        Ok(self)
    }

    /// Provide a factory for the [`TransportConfig`](crate::prelude::TransportConfig) used by the
    /// [`Ditto`](crate::prelude::Ditto) instance.
    pub fn with_transport_config<T>(mut self, factory: T) -> Result<Self, DittoError>
    where
        T: FnOnce(Arc<dyn Identity>) -> TransportConfig,
    {
        match &self.identity {
            Some(id) => {
                let config = factory(id.retain());
                self.transport_config = Some(config)
            }
            None => {
                let msg = "A DittoRoot directory and Identity must first be specified before \
                           providing a custom TransportConfig"
                    .to_string();
                return Err(DittoError::new(ErrorKind::Config, msg));
            }
        }
        Ok(self)
    }

    /// Builds the [`Ditto`](crate::prelude::Ditto) instance, consuming the builder the process.
    pub fn build(self) -> Result<Ditto, DittoError> {
        self.init_logging();
        Self::init_sdk_version();
        let ditto_root = self.ditto_root.ok_or_else(|| {
            DittoError::new(ErrorKind::Config, "No Ditto Root Directory provided")
        })?;

        crate::fs::drain_ditto_data_dir(&ditto_root);

        let c_root_dir = ditto_root.root_dir_to_c_str()?;
        let identity = self
            .identity
            .ok_or_else(|| DittoError::new(ErrorKind::Config, "No Identity specified"))?;
        let auth_client_ref: &ffi_sdk::AuthClient = &identity.auth_client();
        let uninit_ditto = unsafe {
            let executor = ffi_sdk::ditto_auth_extract_executor(auth_client_ref);
            ffi_sdk::uninitialized_ditto_make_with_executor(c_root_dir.as_ref(), executor)
        };
        let boxed_ditto = unsafe {
            ffi_sdk::ditto_make(
                uninit_ditto,
                auth_client_ref,
                ffi_sdk::HistoryTracking::Disabled,
            )
        };
        let ditto: DittoHandleWrapper = Arc::new(boxed_ditto);
        let site_id: SiteId = identity.site_id();
        let store = Store::new(ditto.retain());
        let transport_config = self.transport_config.unwrap_or_else(|| {
            let mut config = TransportConfig::new();
            config.enable_all_peer_to_peer();
            config
        });
        let transports: Arc<RwLock<TransportSync>> = Arc::new(RwLock::new(
            TransportSync::from_config(transport_config, ditto.retain(), identity.retain()),
        ));

        let auth = identity.authenticator();
        let validity_listener = identity.make_listener(Arc::downgrade(&transports));
        let presence = Arc::new(Presence::new(ditto.retain()));

        let disk_usage = DiskUsage::new(ditto.retain(), FsComponent::Root);

        let fields_from_auth = |auth| DittoFields {
            ditto,
            auth,
            identity: identity.retain(),
            store,
            activated: identity.requires_offline_only_license_token().not().into(),
            site_id,
            transports,
            ditto_root,
            validity_listener,
            presence,
            disk_usage,
        };
        let fields = if let Some(mut auth) = auth {
            Arc::new_cyclic(|weak_fields: &arc::Weak<_>| {
                auth.ditto_fields = weak_fields.clone();
                fields_from_auth(Some(auth))
            })
        } else {
            Arc::new(fields_from_auth(None))
        };

        let sdk_ditto = Ditto { fields };
        Ok(sdk_ditto)
    }
}

impl Default for DittoBuilder {
    fn default() -> Self {
        Self::new()
    }
}
