use ropey::Rope;

use crate::{Change, Transaction};

/// Compares `old` and `new` to generate a [`Transaction`] describing
/// the steps required to get from `old` to `new`.
pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
    // `similar` only works on contiguous data, so a `Rope` has
    // to be temporarily converted into a `String`.
    let old_converted = old.to_string();
    let new_converted = new.to_string();

    // A timeout is set so after 1 seconds, the algorithm will start
    // approximating. This is especially important for big `Rope`s or
    // `Rope`s that are extremely dissimilar to each other.
    //
    // Note: Ignore the clippy warning, as the trait bounds of
    // `Transaction::change()` require an iterator implementing
    // `ExactIterator`.
    let mut config = similar::TextDiff::configure();
    config.timeout(std::time::Duration::from_secs(1));

    let diff = config.diff_chars(&old_converted, &new_converted);

    // The current position of the change needs to be tracked to
    // construct the `Change`s.
    let mut pos = 0;
    let changes: Vec<Change> = diff
        .ops()
        .iter()
        .map(|op| op.as_tag_tuple())
        .filter_map(|(tag, old_range, new_range)| {
            // `old_pos..pos` is equivalent to `start..end` for where
            // the change should be applied.
            let old_pos = pos;
            pos += old_range.end - old_range.start;

            match tag {
                // Semantically, inserts and replacements are the same thing.
                similar::DiffTag::Insert | similar::DiffTag::Replace => {
                    // This is the text from the `new` rope that should be
                    // inserted into `old`.
                    let text: &str = {
                        let start = new.char_to_byte(new_range.start);
                        let end = new.char_to_byte(new_range.end);
                        &new_converted[start..end]
                    };
                    Some((old_pos, pos, Some(text.into())))
                }
                similar::DiffTag::Delete => Some((old_pos, pos, None)),
                similar::DiffTag::Equal => None,
            }
        })
        .collect();
    Transaction::change(old, changes.into_iter())
}

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

    quickcheck::quickcheck! {
        fn test_compare_ropes(a: String, b: String) -> bool {
            let mut old = Rope::from(a);
            let new = Rope::from(b);
            compare_ropes(&old, &new).apply(&mut old);
            old.to_string() == new.to_string()
        }
    }
}
