use_prelude!();

pub mod document;
pub mod document_id;
pub mod pending_cursor_operation;
pub mod pending_id_specific_operation;
pub mod type_traits;

use std::mem::MaybeUninit;

use ::ffi_sdk::{Attachment, AttachmentFileOperation};
pub use document::*;
use pending_cursor_operation::*;
use pending_id_specific_operation::*;

use super::{
    ditto_attachment::DittoAttachment, ditto_attachment_fetch_event::DittoAttachmentFetchEvent,
    ditto_attachment_fetcher::DittoAttachmentFetcher, ditto_attachment_token::DittoAttachmentToken,
};
use crate::{
    ditto::WeakDittoHandleWrapper,
    error::{DittoError, ErrorKind},
};

#[derive(Clone, Debug)]
/// A reference to a collection in a [`Store`](crate::prelude::Store).
pub struct Collection {
    pub(super) ditto: WeakDittoHandleWrapper,
    pub(super) collection_name: char_p::Box,
}

impl Collection {
    pub(crate) fn new(ditto: WeakDittoHandleWrapper, collection_name: String) -> Self {
        let collection_name = char_p::new(collection_name.as_str());
        Self {
            ditto,
            collection_name,
        }
    }

    /// The name of the collection.
    pub fn name(&self) -> &str {
        self.collection_name.as_ref().to_str()
    }

    /// Generates a [`PendingCursorOperation`](crate::prelude::PendingCursorOperation) that can be
    /// used to find all documents in the collection at a point in time or you can chain a call to
    /// [`observe_local`](crate::prelude::PendingCursorOperation::observe_local) or
    /// [`subscribe`](crate::prelude::PendingCursorOperation::subscribe) if you want to get updates
    /// about documents in the collection over time. It can also be used to update, remove, or evict
    /// documents.
    pub fn find_all(&self) -> PendingCursorOperation<'_> {
        self.find("true")
    }

    /// Generates a [`PendingCursorOperation`]((crate::prelude::PendingCursorOperation)) with the
    /// provided query that can be used to find the documents matching the query at a point in time
    /// or you can chain a call to
    /// [`observe_local`](crate::prelude::PendingCursorOperation::observe_local) or
    /// [`subscribe`](crate::prelude::PendingCursorOperation::subscribe) if you want to get updates
    /// about documents matching the query as they occur. It can also be used to update, remove, or
    /// evict documents.
    pub fn find(&self, query: &'_ str) -> PendingCursorOperation<'_> {
        PendingCursorOperation::<'_>::new(
            self.ditto.clone(),
            self.collection_name.to_owned(),
            query,
            None,
        )
    }

    /// Generates a [`PendingCursorOperation`](crate::prelude::PendingCursorOperation) with the
    /// provided query and query arguments that can be used to find the documents matching the query
    /// at a point in time or you can chain a call to
    /// [`observe_local`](crate::prelude::PendingCursorOperation::observe_local) or
    /// [`subscribe`](crate::prelude::PendingCursorOperation::subscribe) if you want to get updates
    /// about documents matching the query as they occur. It can also be used to update, remove, or
    /// evict documents.
    ///
    /// This is the recommended function to use when performing queries on a collection if you have
    /// any dynamic data included in the query string. It allows you to provide a query string with
    /// placeholders, in the form of `$args.my_arg_name`, along with an accompanying dictionary of
    /// arguments, in the form of `{ "my_arg_name": "some value" }`, and the placeholders will be
    /// appropriately replaced by the matching provided arguments from the dictionary. This includes
    /// handling things like wrapping strings in quotation marks and arrays in square brackets, for
    /// example.
    pub fn find_with_args<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        query: &'_ str,
        query_args: C,
    ) -> PendingCursorOperation<'_> {
        PendingCursorOperation::<'_>::new(
            self.ditto.clone(),
            self.collection_name.to_owned(),
            query,
            Some(serde_cbor::to_vec(query_args.borrow()).unwrap()),
        )
    }

    /// Generates a [`PendingIdSpecificOperation`](crate::prelude::PendingIdSpecificOperation) with
    /// the provided document Id that can be used to find the document at a point in time or you can
    /// chain a call to [`observe_local`](crate::prelude::PendingIdSpecificOperation::observe_local)
    /// or [`subscribe`](crate::prelude::PendingIdSpecificOperation::subscribe) if you want to get
    /// updates about the document over time. It can also be used to update, remove, or evict the
    /// document.
    pub fn find_by_id(&self, doc_id: impl Into<DocumentId>) -> PendingIdSpecificOperation {
        PendingIdSpecificOperation {
            ditto: self.ditto.clone(),
            collection_name: self.collection_name.to_owned(),
            doc_id: doc_id.into(),
        }
    }
}

/// Convenience impl to avoid having to type `.clone()` everywhere.
impl From<&DocumentId> for DocumentId {
    fn from(doc: &DocumentId) -> Self {
        doc.clone()
    }
}

impl Collection {
    /// Inserts a new document into the collection and returns its Id. If the
    /// document already exists, the provided document content will be merged
    /// with the existing document's content.
    pub fn upsert<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        content: C,
    ) -> Result<DocumentId, DittoError> {
        self.insert_cbor(
            ::serde_cbor::to_vec(content.borrow()).unwrap().as_slice(),
            None,
            WriteStrategy::Merge,
            None,
        )
    }

    /// Inserts a new document into the collection and returns its Id. If the
    /// document already exists, the behavior is determined by the provided
    /// `write_strategy`.
    pub fn upsert_with_strategy<V: ::serde::Serialize, C: Borrow<V>>(
        &self,
        content: C,
        write_strategy: WriteStrategy,
    ) -> Result<DocumentId, DittoError> {
        self.insert_cbor(
            ::serde_cbor::to_vec(content.borrow()).unwrap().as_slice(),
            None,
            write_strategy,
            None,
        )
    }

    /// Inserts a new document into the collection and returns its assigned Id.
    /// Use this method when the content has already been serialized externally
    /// into a CBOR-formatted byte array
    pub(crate) fn insert_cbor(
        &self,
        cbor: &'_ [u8],
        id: Option<&DocumentId>,
        write_strategy: WriteStrategy,
        write_txn: Option<&'_ mut ffi_sdk::CWriteTransaction>,
    ) -> Result<DocumentId, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        let id: Option<c_slice::Ref<'_, u8>> = id.map(|id| id.as_ref().into());
        let write_strategy_rs = write_strategy.as_write_strategy_rs();

        let hint: Option<char_p::Ref<'_>> = None;
        let id = unsafe {
            ffi_sdk::ditto_collection_insert_value(
                &*ditto,
                self.collection_name.as_ref(),
                cbor.into(),
                id,
                write_strategy_rs,
                hint,
                write_txn,
            )
        }
        .ok_or(ErrorKind::InvalidInput)?;

        Ok(id.to::<Box<[u8]>>().into())
    }

    /// Creates a new attachment, which can then be inserted into a document.
    ///
    /// The file residing at the provided path will be copied into the Ditto’s store. The
    /// [`DittoAttachment`](crate::prelude::DittoAttachment) object that is returned is what you can
    /// then use to insert an attachment into a document.
    ///
    /// You can provide metadata about the attachment, which will be replicated to other peers
    /// alongside the file attachment.
    ///
    /// Below is a snippet to show how you can use the new_attachment functionality to insert an
    /// attachment into a document.
    pub fn new_attachment<P: AsRef<Path>>(
        &self,
        path: P,
        metadata: HashMap<String, String>,
    ) -> Result<DittoAttachment, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        use ::safer_ffi::prelude::AsOut;
        let source_path = char_p::new(path.as_ref().to_str().unwrap());
        let file_operation = AttachmentFileOperation::Copy;
        let mut slot = MaybeUninit::<Attachment>::uninit();
        let out_attachment: Out<'_, Attachment> = slot.as_out();
        let status = unsafe {
            ffi_sdk::ditto_new_attachment_from_file(
                &ditto,
                source_path.as_ref(),
                file_operation,
                out_attachment,
            )
        };
        if status != 0 {
            Err(DittoError::from_ffi(ErrorKind::InvalidInput))
        } else {
            let attachment = unsafe { slot.assume_init() }; // safe assuming above ffi call was successful
            let ret = DittoAttachment::new(
                attachment.id.into(),
                attachment.len,
                metadata,
                self.ditto.clone(),
                attachment.handle,
            );
            Ok(ret)
        }
    }

    /// Fetch the attachment corresponding to the provided attachment token.
    ///
    /// - `on_fetch_event`: A closure that will be called when the status of the request to fetch
    ///   the attachment has changed. If the attachment is already available then this will be
    ///   called almost immediately with a completed status value.
    pub fn fetch_attachment<'a>(
        &self,
        attachment_token: DittoAttachmentToken,
        on_fetch_event: impl Fn(DittoAttachmentFetchEvent) + Send + Sync + 'a,
    ) -> Result<DittoAttachmentFetcher<'a>, DittoError> {
        let ditto = self
            .ditto
            .upgrade()
            .ok_or(crate::error::ErrorKind::ReleasedDittoInstance)?;
        DittoAttachmentFetcher::new(attachment_token, ditto, on_fetch_event)
    }
}
