//! Use attachments to sync large binary files between peers.

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
    sync::{Arc, Weak},
};

use ffi_sdk::{BoxedAttachmentHandle, BoxedDitto};
use safer_ffi::prelude::*;
use serde::ser::SerializeMap;

use crate::{ditto::TryUpgrade, prelude::DittoError, utils::prelude::ErrorKind};

mod fetch_event;
pub use self::fetch_event::DittoAttachmentFetchEvent;

mod fetcher;
pub use self::fetcher::{DittoAttachmentFetcher, FetcherVersion};

mod token;
pub use self::token::{DittoAttachmentToken, DittoAttachmentTokenLike};

/// Represents an attachment and can be used to insert the associated attachment into a document at
/// a specific key.
#[derive(Debug)]
pub struct DittoAttachment {
    id: Box<[u8]>,
    len: u64,
    metadata: HashMap<String, String>,
    ditto: Weak<BoxedDitto>,
    attachment_handle: BoxedAttachmentHandle,
}

impl DittoAttachment {
    /// Returns the `id` of this attachment, encoded so as to be compatible with DQL queries.
    pub fn id(&self) -> String {
        crate::utils::base64_encode_unpadded(&self.id)
    }

    /// Returns the `len`, in bytes, of this attachment's data.
    #[allow(clippy::len_without_is_empty)]
    pub fn len(&self) -> u64 {
        self.len
    }

    /// Returns the metadata that was associated with this attachment file when the source peer
    /// called [`new_attachment`][crate::store::Store::new_attachment].
    pub fn metadata(&self) -> &HashMap<String, String> {
        &self.metadata
    }
}

impl serde::Serialize for DittoAttachment {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut map = serializer.serialize_map(Some(4))?;
        map.serialize_entry(
            // This breaks compatibility with the Rust SDK prior
            // to versions 3.0.8 and 4.0.0. It is necessary for
            // cross-SDK attachments compatibility.
            "_ditto_internal_type_jkb12973t4b",
            &(::ffi_sdk::DittoCrdtType::Attachment as u64),
        )?;
        map.serialize_entry("_id", ::serde_bytes::Bytes::new(&self.id[..]))?;
        map.serialize_entry("_len", &self.len)?;
        map.serialize_entry("_meta", &self.metadata)?;
        map.end()
    }
}

impl DittoAttachment {
    /// Create a new DittoAttachment
    pub(crate) fn new(
        id: Box<[u8]>,
        len: u64,
        metadata: HashMap<String, String>,
        ditto: Weak<BoxedDitto>,
        attachment_handle: BoxedAttachmentHandle,
    ) -> Self {
        Self {
            id,
            len,
            metadata,
            ditto,
            attachment_handle,
        }
    }

    pub(crate) fn from_file_and_metadata(
        filepath: &(impl ?Sized + AsRef<Path>),
        metadata: HashMap<String, String>,
        ditto: &Arc<ffi_sdk::BoxedDitto>,
    ) -> Result<DittoAttachment, DittoError> {
        let source_path = char_p::new(filepath.as_ref().to_str().unwrap());
        let file_operation = ffi_sdk::AttachmentFileOperation::Copy;
        let mut slot = ::core::mem::MaybeUninit::<ffi_sdk::Attachment>::uninit();
        let status = {
            ffi_sdk::ditto_new_attachment_from_file(
                ditto,
                source_path.as_ref(),
                file_operation,
                slot.as_out(),
            )
        };
        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,
                Arc::downgrade(ditto),
                attachment.handle,
            );
            Ok(ret)
        }
    }

    pub(crate) fn from_bytes_and_metadata(
        bytes: &(impl ?Sized + AsRef<[u8]>),
        metadata: HashMap<String, String>,
        ditto: &Arc<ffi_sdk::BoxedDitto>,
    ) -> Result<DittoAttachment, DittoError> {
        let mut slot = ::core::mem::MaybeUninit::<ffi_sdk::Attachment>::uninit();
        let status = {
            ffi_sdk::ditto_new_attachment_from_bytes(ditto, bytes.as_ref().into(), slot.as_out())
        };
        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,
                Arc::downgrade(ditto),
                attachment.handle,
            );
            Ok(ret)
        }
    }

    /// Create a new DittoAttachment from a Token
    pub(crate) fn new_with_token(
        token: DittoAttachmentToken,
        ditto: Weak<BoxedDitto>,
        attachment_handle: BoxedAttachmentHandle,
    ) -> Self {
        Self {
            id: token.id,
            len: token.len,
            metadata: token.metadata,
            ditto,
            attachment_handle,
        }
    }

    /// Return path to an attachment.
    /// # Panics
    /// Panics if Ditto has been released
    pub fn path(&self) -> PathBuf {
        // FIXME(Ronan) Ideally wrap this function in a Result
        let ditto = self.ditto.try_upgrade().unwrap();
        let p = ffi_sdk::ditto_get_complete_attachment_path(&ditto, &self.attachment_handle);
        let p_string = p.to_string();
        p_string.into()
    }
}
