use std::{collections::HashSet, fmt};

use ffi_sdk::{self, ffi_utils::repr_c};

use crate::dql::QueryResultItem;

/// Describes the move of an item in a [`Diff`].
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DiffMove {
    /// The index in the list of "old items" that the item has moved from.
    pub from: usize,

    /// The index in the list of "new items" that the item has moved to.
    pub to: usize,
}

impl From<(usize, usize)> for DiffMove {
    fn from((from, to): (usize, usize)) -> Self {
        Self { from, to }
    }
}

/// Represents a diff between two arrays.
///
/// Create a diff between arrays of [`QueryResultItem`]s using a [`Differ`].
///
/// Alternatively, you can directly create a [`Diff`] via [`Diff::empty`]:
/// ```rust
/// # use dittolive_ditto::dql::Diff;
/// let mut diff = Diff::empty();
/// diff.insertions.extend([1, 2, 3]);
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[non_exhaustive]
pub struct Diff {
    /// The set of indexes in the _new_ array at which new items have been inserted.
    pub insertions: HashSet<usize>,

    /// The set of indexes in the _old_ array at which old items have been deleted.
    pub deletions: HashSet<usize>,

    /// The set of indexes in the _new_ array at which items have been updated.
    pub updates: HashSet<usize>,

    /// A set of tuples each representing a move of an item `from` a particular
    /// index in the _old_ array `to` a particular index in the _new_ array.
    pub moves: HashSet<DiffMove>,
}

impl Diff {
    /// Create a new empty [Diff] instance.
    ///
    /// This is equivalent to calling `Diff::default()`.
    pub fn empty() -> Self {
        Self::default()
    }
}

/// Calculates diffs between arrays of [`QueryResultItem`]s.
///
/// Use a [`Differ`] with a
/// [`StoreObserver`][crate::store::StoreObserver] to get the diff
/// between subsequent query results delivered by the store observer.
pub struct Differ {
    raw: repr_c::Box<ffi_sdk::FfiDiffer>,
}

impl Differ {
    /// Create a new [`Differ`] instance.
    #[inline]
    pub fn new() -> Self {
        Self {
            raw: ffi_sdk::dittoffi_differ_new(),
        }
    }

    /// Calculate the diff of the provided items against the last set of items
    /// that were passed to this differ.
    ///
    /// The returned [`Diff`] identifies changes from the old array of items
    /// to the new array of items using indices into both arrays.
    ///
    /// Initially, the differ has no items, so the first call to this method
    /// will always return a diff showing all items as insertions.
    ///
    /// The identity of items is determined by their `_id` field.
    ///
    /// - `items` The query result items to compare against the last array of items.
    pub fn diff(&self, items: impl IntoIterator<Item = QueryResultItem>) -> Diff {
        let raw_items: Vec<_> = items.into_iter().map(|item| item.raw.clone()).collect();
        let diff_cbor = ffi_sdk::dittoffi_differ_diff(&self.raw, raw_items.as_slice().into());
        let deserialized: serialization::SerializedDiff =
            ::serde_cbor::from_slice(&diff_cbor.0).unwrap();
        Diff {
            insertions: deserialized.insertions.into_iter().collect(),
            deletions: deserialized.deletions.into_iter().collect(),
            updates: deserialized.updates.into_iter().collect(),
            moves: deserialized
                .moves
                .into_iter()
                .map(|m| DiffMove::from((m.from, m.to)))
                .collect(),
        }
    }
}

impl fmt::Debug for Differ {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Differ").finish()
    }
}

impl Default for Differ {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

/// Private module for serialization and deserialization of [Diff]
mod serialization {
    /// Model for serialized CBOR data for a [Diff].
    #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
    pub(super) struct SerializedDiff {
        pub(super) insertions: Vec<usize>,
        pub(super) deletions: Vec<usize>,
        pub(super) updates: Vec<usize>,
        pub(super) moves: Vec<SerializedDiffMove>,
    }

    /// Model for serialized CBOR data for a [DiffMove] element of a [Diff].
    #[derive(Debug, PartialEq, Eq)]
    pub(super) struct SerializedDiffMove {
        pub(super) from: usize,
        pub(super) to: usize,
    }

    impl From<(usize, usize)> for SerializedDiffMove {
        fn from((from, to): (usize, usize)) -> Self {
            Self { from, to }
        }
    }

    // A DiffMove gets deserialized from a 2-element array where the first element is the "from"
    // index and the second element is the "to" index.
    impl<'de> serde::Deserialize<'de> for SerializedDiffMove {
        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
        where
            D: serde::de::Deserializer<'de>,
        {
            let arr: Vec<usize> = serde::Deserialize::deserialize(deserializer)?;
            if arr.len() != 2 {
                return Err(serde::de::Error::custom(
                    "DiffMove must be a 2-element array",
                ));
            }
            Ok(SerializedDiffMove {
                from: arr[0],
                to: arr[1],
            })
        }
    }

    impl serde::Serialize for SerializedDiffMove {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: serde::Serializer,
        {
            use serde::ser::SerializeSeq;
            let mut state = serializer.serialize_seq(Some(2))?;
            state.serialize_element(&self.from)?;
            state.serialize_element(&self.to)?;
            state.end()
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn diff_serialization_and_deserialization_works() {
            let diff = SerializedDiff {
                insertions: [0, 1, 2].into(),
                deletions: [3, 4].into(),
                updates: [5].into(),
                moves: vec![(6, 7), (8, 9)].into_iter().map(Into::into).collect(),
            };

            let cbor_data = serde_cbor::to_vec(&diff).unwrap();

            let deserialized_diff: SerializedDiff = serde_cbor::from_slice(&cbor_data).unwrap();

            assert_eq!(diff.insertions, deserialized_diff.insertions);
            assert_eq!(diff.deletions, deserialized_diff.deletions);
            assert_eq!(diff.updates, deserialized_diff.updates);
            assert_eq!(diff.moves, deserialized_diff.moves);
        }
    }
}
