//! [`Store`] is a holder to create, read, write, and remove
//! [`Document`](crate::store::collection::document::DittoDocument)s from a Ditto peer.

use_prelude!();

use ffi_sdk::{COrderByParam, FsComponent, WriteStrategyRs};

use crate::{
    disk_usage::DiskUsage,
    error::{DittoError, ErrorKind},
};

pub mod batch;
pub mod collection;
pub mod collections;
pub mod ditto_attachment;
pub mod ditto_attachment_fetch_event;
pub mod ditto_attachment_fetcher;
pub mod ditto_attachment_token;
pub mod live_query;
#[cfg(feature = "timeseries")]
pub mod timeseries;
pub mod update;

use collections::pending_collections_operation::PendingCollectionsOperation;

#[derive(Clone)]
/// `Store` provides access to [`Collection`](crate::prelude::Collection)s and a
/// write transaction API.
pub struct Store {
    ditto: Arc<ffi_sdk::BoxedDitto>,
    disk_usage: Arc<DiskUsage>,
}

impl Store {
    // TODO(pub_check)
    pub fn new(ditto: Arc<ffi_sdk::BoxedDitto>) -> Self {
        let disk_usage = Arc::new(DiskUsage::new(ditto.retain(), FsComponent::Store));
        Self { ditto, disk_usage }
    }

    // Note this method's logic will be moved into the core ditto library
    // in the future
    fn validate_collection_name(name: &str) -> Result<(), DittoError> {
        let mut result = Ok(());

        if name.is_empty() {
            result = Err(DittoError::new(
                ErrorKind::InvalidInput,
                String::from("Collection name can not be empty"),
            ));
        }

        if name.split_whitespace().next().is_none() {
            result = Err(DittoError::new(
                ErrorKind::InvalidInput,
                String::from("Collection name can not only contain whitespace"),
            ));
        }

        result
    }

    /// Returns a [`Collection`](crate::prelude::Collection) with the provided name.
    pub fn collection(&self, collection_name: &'_ str) -> Result<Collection, DittoError> {
        Self::validate_collection_name(collection_name)?;
        let c_name = char_p::new(collection_name);
        let status = unsafe { ffi_sdk::ditto_collection(&*self.ditto, c_name.as_ref()) };
        if status != 0 {
            return Err(DittoError::from_ffi(ErrorKind::InvalidInput));
        }
        Ok(Collection {
            ditto: Arc::downgrade(&self.ditto),
            collection_name: c_name,
        })
    }

    /// Returns an object that lets you fetch or observe the collections in the
    /// store.
    pub fn collections(&self) -> PendingCollectionsOperation<'_> {
        PendingCollectionsOperation::<'_>::new(Arc::downgrade(&self.ditto))
    }

    /// Allows you to group multiple operations together that affect multiple
    /// documents, potentially across multiple collections, without
    /// auto-committing on each operation.
    ///
    /// At the end of the batch of operations, either
    /// [`batch.commit_changes`](crate::store::batch::ScopedStore::commit_changes)
    /// or
    /// [`batch.revert_changes`](crate::store::batch::ScopedStore::revert_changes)
    /// must be called.
    ///
    /// ## Example
    ///
    /// ```rust
    /// # macro_rules! ignore {($($__:tt)*) => ()} ignore! {
    /// ditto.store().with_batched_write(|batch| {
    ///     let mut foo_coll = batch.collection("foo");
    ///     foo_coll.find...().remove();
    ///     let mut bar_coll = batch.collection("bar");
    ///     // Expensive multi-mutation op:
    ///     for _ in 0 .. 10_000 {
    ///         let doc = ...;
    ///         bar_coll.insert(doc, None, false);
    ///     }
    ///     // At this point, we must say whether we commit or revert
    ///     // these changes:
    ///     batch.commit_changes()
    /// })
    /// # }
    /// ```
    pub fn with_batched_write<F>(
        &self,
        f: F,
    ) -> Result<Vec<batch::WriteTransactionResult>, DittoError>
    where
        for<'batch> F: FnOnce(batch::ScopedStore<'batch>) -> batch::Action<'batch>,
    {
        batch::with_batched_write(self, f)
    }

    /// Returns a list of the names of collections in the local store.
    pub fn collection_names(&self) -> Result<Vec<String>, DittoError> {
        let c_collections = unsafe { ffi_sdk::ditto_get_collection_names(&*self.ditto).ok()? };

        Ok(c_collections
            .iter()
            .map(|x: &char_p::Box| -> String { x.clone().into_string() })
            .collect())
    }

    /// Returns a hash representing the current version of the given queries.
    /// When a document matching such queries gets mutated, the hash will change
    /// as well.
    ///
    /// Please note that the hash depends on how queries are constructed, so you
    /// should make sure to always compare hashes generated with the same set of
    /// queries.
    pub fn queries_hash(&self, live_queries: &[LiveQuery]) -> Result<u64, DittoError> {
        let (coll_names, queries): (Vec<_>, Vec<_>) = live_queries
            .iter()
            .map(|lq| (lq.collection_name.as_ref(), lq.query.as_ref()))
            .unzip();

        unsafe {
            ffi_sdk::ditto_queries_hash(&self.ditto, coll_names[..].into(), queries[..].into()).ok()
        }
    }

    /// Returns a sequence of English words representing the current version of
    /// the given queries. When a document matching such queries gets mutated,
    /// the words will change as well.
    ///
    /// Please note that the resulting sequence of words depends on how queries
    /// are constructed, so you should make sure to always compare hashes
    /// generated with the same set of queries.
    pub fn queries_hash_mnemonic(&self, live_queries: &[LiveQuery]) -> Result<String, DittoError> {
        let (coll_names, queries): (Vec<_>, Vec<_>) = live_queries
            .iter()
            .map(|lq| (lq.collection_name.as_ref(), lq.query.as_ref()))
            .unzip();

        unsafe {
            ffi_sdk::ditto_queries_hash_mnemonic(
                &self.ditto,
                coll_names[..].into(),
                queries[..].into(),
            )
            .ok()
            .map(|c_str| c_str.unwrap().into_string())
        }
    }

    /// Start all live query webhooks.
    pub fn start_all_live_query_webhooks(&self) -> Result<(), DittoError> {
        unsafe {
            let ret = ffi_sdk::ditto_live_query_webhook_start_all(&self.ditto);
            if ret != 0 {
                return Err(DittoError::from_ffi(ErrorKind::Internal));
            }
        }
        Ok(())
    }

    /// Start a live query webhooks by its id.
    pub fn start_live_query_webhook_by_id(&self, doc_id: DocumentId) -> Result<(), DittoError> {
        unsafe {
            let ret =
                ffi_sdk::ditto_live_query_webhook_start_by_id(&self.ditto, doc_id.bytes[..].into());
            if ret != 0 {
                return Err(DittoError::from_ffi(ErrorKind::Internal));
            }
        }
        Ok(())
    }

    /// Register a new live query webhook
    pub fn register_live_query_webhook(
        &self,
        collection_name: &str,
        query: &str,
        url: &str,
    ) -> Result<DocumentId, DittoError> {
        let c_collection_name = char_p::new(collection_name);
        let c_query = char_p::new(query);
        let c_url = char_p::new(url);
        let order_definitions: Vec<COrderByParam<'_>> = Vec::with_capacity(0);
        let doc_id = unsafe {
            ffi_sdk::ditto_live_query_webhook_register_str(
                &self.ditto,
                c_collection_name.as_ref(),
                c_query.as_ref(),
                order_definitions[..].into(),
                -1,
                0,
                c_url.as_ref(),
            )
            .ok()?
            .to::<Box<[u8]>>()
            .into()
        };

        Ok(doc_id)
    }

    /// Generate a new API secret for live query webhook
    pub fn live_query_webhook_generate_new_api_secret(&self) -> Result<(), DittoError> {
        unsafe {
            let ret = ffi_sdk::ditto_live_query_webhook_generate_new_api_secret(&self.ditto);
            if ret != 0 {
                return Err(DittoError::from_ffi(ErrorKind::Internal));
            }
        }
        Ok(())
    }

    /// Returns a [`TimeSeries`](crate::prelude::TimeSeries) with the provided name.
    pub fn timeseries(&self, ts_name: &'_ str) -> Result<TimeSeries, DittoError> {
        Self::validate_collection_name(ts_name)?;
        let c_name = char_p::new(ts_name);
        Ok(TimeSeries {
            ditto: self.ditto.retain(),
            ts_name: c_name,
        })
    }

    /// Return a [`DiskUsage`](crate::prelude::DiskUsage) to monitor the disk usage of the
    /// [`Store`].
    pub fn disk_usage(&self) -> &DiskUsage {
        &self.disk_usage
    }
}

#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
/// Specify the order of returned Documents in a query.
pub enum SortDirection {
    Ascending,
    Descending,
}

#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
/// Specify the write strategy when inserting documents.
pub enum WriteStrategy {
    /// An existing document will be merged with the document being inserted, if there is a
    /// pre-existing document.
    Merge,

    /// Insert the document only if there is not already a document with the same Id in the store.
    /// If there is already a document in the store with the same Id then this will be a no-op.
    InsertIfAbsent,

    /// Insert the document, with its contents treated as default data, only if there is not
    /// already a document with the same Id in the store. If there is already a document in the
    /// store with the same Id then this will be a no-op. Use this strategy if you want to
    /// insert default data for a given document Id, which you want to treat as common initial
    /// data amongst all peers and that you expect to be mutated or overwritten in due course.
    InsertDefaultIfAbsent,
}

impl WriteStrategy {
    fn as_write_strategy_rs(&self) -> WriteStrategyRs {
        match self {
            WriteStrategy::Merge => WriteStrategyRs::Merge,
            WriteStrategy::InsertIfAbsent => WriteStrategyRs::InsertIfAbsent,
            WriteStrategy::InsertDefaultIfAbsent => WriteStrategyRs::InsertDefaultIfAbsent,
        }
    }
}
