//! Documents are containers for the data you put in a
//! `[Collection`](super::collection::Collection).
//!
//! Documents exist in two ways:
//! - [`DittoDocument`] is a static view of the content of a document.
//! - [`DittoMutDocument`] which allows to update the content of a document.

#![allow(deprecated)]

use_prelude!();
use std::{borrow::BorrowMut, time::SystemTime};

use ffi_sdk::PathAccessorType;
use serde::{de::DeserializeOwned, Serialize};

use super::type_traits::MutableValue;
use crate::error::{DittoError, ErrorKind};

// Similar to an ObjC Extension, DittoDocument can be implemented
// for a number of opaque types (usually various smart pointers)
/// [`DittoDocument`]s can be obtained via calls to the various
/// [`exec`](crate::prelude::PendingIdSpecificOperation::exec)
/// [`methods`](crate::prelude::PendingCursorOperation::exec) or via the various
/// [`observe_local`](crate::prelude::PendingIdSpecificOperation::observe_local)
/// [methods](crate::prelude::PendingCursorOperation::observe_local), exposed via the objects
/// returned from calls to [`Collection::find_by_id`](crate::prelude::Collection::find_by_id),
/// [`Collection::find`](crate::prelude::Collection::find), and
/// [`Collection::find_all`](crate::prelude::Collection::find_all).
///
/// This is the readonly view of the document as it is currently stored in the
/// [`Collection`] and it will not be updated if the document is changed.
#[deprecated(
    since = "4.14.4",
    note = "Will be removed in SDK 5.0. Use DQL queries instead"
)]
pub trait DittoDocument {
    /// Returns the [`DocumentId`] of the document.
    fn id(&self) -> DocumentId;

    /// Renders the document as a CBOR value.
    fn to_cbor(&self) -> Result<serde_cbor::Value, DittoError>;

    /// Returns the value at a given path in the document.
    ///
    /// The parameter `V` represents the type expected at the given path. Note, however, that other
    /// peers may have changed this type and deserialization may fail. In this case provide a
    /// general type like `serde_cbor::Value`.
    fn get<V: DeserializeOwned>(&self, path: &str) -> Result<V, DittoError>;

    /// Decodes a document into a user-provided type provided that type implements
    /// `serde::Deserialize`.
    ///
    /// Note that this is a snapshot of the current state of the document, which may be later
    /// modified by other peers
    fn typed<T: DeserializeOwned>(&self) -> Result<T, DittoError>;
}

/// [`DittoMutDocument`]s can be obtained within `update` calls
/// ([`PendingIdSpecificOperation::update`](crate::prelude::PendingIdSpecificOperation::update) or
/// [`PendingCursorSpecificOperation::update`](crate::prelude::PendingCursorOperation::update)).
#[deprecated(
    since = "4.14.4",
    note = "Will be removed in SDK 5.0. Use DQL queries instead"
)]
pub trait DittoMutDocument: DittoDocument {
    /// Returns the value at a given path in the mutable document.
    ///
    /// The parameter `V` represents the type expected at the given path. Note however that other
    /// peers may have changed this type and deserialization may fail.
    // FIXME(Daniel): does this apply in this case ??
    // In this case provide a general type like `serde_cbor::Value`
    fn get_mut<'doc, V: MutableValue<'doc>>(&'doc mut self, path: &str) -> Result<V, DittoError>;

    /// Assigns a some serializable value to the document at the given path *and removes any other
    /// information present at that path*.
    ///
    /// If the path does not exist it will be created.
    fn set<V: Serialize>(&mut self, path: &str, val: V) -> Result<(), DittoError>;

    /// Sets the value at the provided path and marks it as the default, indicating other peers in
    /// the network are expected to overwrite it.
    fn set_as_default<V: Serialize>(&mut self, path: &str, val: V) -> Result<(), DittoError>;

    /// Remove the value from a document at a given path
    fn remove(&mut self, path: &str) -> Result<(), DittoError>;

    /// Increment (+/-) a counter by `amount`.
    fn increment(&mut self, path: &str, amount: f64) -> Result<(), DittoError>;
}

// impl DittoDocument for ffi_sdk::DocumentRef { // a Box<Document>
impl<T: Borrow<ffi_sdk::Document>> DittoDocument for T {
    fn id(&self) -> DocumentId {
        let id_slice = ffi_sdk::ditto_document_id(self.borrow());
        Vec::<u8>::from(Box::<[u8]>::from(id_slice)).into()
    }

    fn to_cbor(&self) -> Result<serde_cbor::Value, DittoError> {
        let c_bytes = ffi_sdk::ditto_document_cbor(self.borrow());
        let result = serde_cbor::from_slice(c_bytes.as_slice())
            .map_err(|e| DittoError::new(ErrorKind::InvalidInput, e));
        result
    }

    fn get<V: DeserializeOwned>(&self, path: &str) -> Result<V, DittoError> {
        get_internal(self.borrow(), path, PathAccessorType::Any)
    }

    fn typed<V: DeserializeOwned>(&self) -> Result<V, DittoError> {
        let c_bytes = ffi_sdk::ditto_document_cbor(self.borrow());
        let result = serde_cbor::from_slice::<V>(c_bytes.as_slice())
            .map_err(|e| DittoError::new(ErrorKind::InvalidInput, e));
        result
    }
}

fn get_internal<V: DeserializeOwned>(
    doc: &ffi_sdk::Document,
    path: &str,
    path_type: PathAccessorType,
) -> Result<V, DittoError> {
    let c_path = char_p::new(path);
    let value = ::serde_cbor::from_slice(
        ::ffi_sdk::ditto_document_get_cbor_with_path_type(doc, c_path.as_ref(), path_type)
            .ok_or(ErrorKind::InvalidInput)?
            .ok_or(ErrorKind::NonExtant)?
            .as_slice(),
    )
    .map_err(|e| DittoError::new(ErrorKind::InvalidInput, e))?;
    Ok(value)
}

impl<T: BorrowMut<ffi_sdk::Document>> DittoMutDocument for T {
    /// Retrieve the mutable version of any ditto mutable explicit type
    fn get_mut<'doc, V: MutableValue<'doc>>(&'doc mut self, path: &str) -> Result<V> {
        let base_value: V::BaseType = self.get(path)?;
        let doc = self.borrow_mut();
        let c_key = char_p::new(path);
        V::mutable_version(base_value, doc, c_key)
    }

    fn set<V: Serialize>(&mut self, path: &str, val: V) -> Result<(), DittoError> {
        let c_str = char_p::new(path);
        let cbor: Vec<u8> =
            serde_cbor::to_vec(&val).map_err(|e| DittoError::new(ErrorKind::InvalidInput, e))?;
        let status = {
            ffi_sdk::ditto_document_set_cbor(
                self.borrow_mut(),
                c_str.as_ref(),
                cbor.as_slice().into(),
            )
        };
        if status != 0 {
            Err(DittoError::from_ffi(ErrorKind::InvalidInput))
        } else {
            Ok(())
        }
    }

    fn set_as_default<V: Serialize>(&mut self, path: &str, val: V) -> Result<(), DittoError> {
        let c_str = char_p::new(path);
        let cbor: Vec<u8> =
            serde_cbor::to_vec(&val).map_err(|e| DittoError::new(ErrorKind::InvalidInput, e))?;

        // This should be reasonably stable for this application, otherwise we can
        // upgrade to the chrono library
        // Note the underlying impl is platform specific
        let timestamp = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
            Ok(n) => n.as_secs(),
            Err(e) => return Err(DittoError::new(ErrorKind::Internal, e)),
        };
        let status = {
            ffi_sdk::ditto_document_set_cbor_with_timestamp(
                self.borrow_mut(),
                c_str.as_ref(),
                cbor.as_slice().into(),
                timestamp as u32,
            )
        };
        if status != 0 {
            Err(DittoError::from_ffi(ErrorKind::InvalidInput))
        } else {
            Ok(())
        }
    }

    fn remove(&mut self, path: &str) -> Result<(), DittoError> {
        let pointer = char_p::new(path);
        let status = ffi_sdk::ditto_document_remove(self.borrow_mut(), pointer.as_ref());
        if status != 0 {
            Err(DittoError::from_ffi(ErrorKind::InvalidInput))
        } else {
            Ok(())
        }
    }

    fn increment(&mut self, path: &str, amount: f64) -> Result<(), DittoError> {
        let pointer = char_p::new(path);

        let status = {
            ffi_sdk::ditto_document_increment_counter(self.borrow_mut(), pointer.as_ref(), amount)
        };
        if status != 0 {
            Err(DittoError::from_ffi(ErrorKind::InvalidInput))
        } else {
            Ok(())
        }
    }
}
