//! Utilities for common diff related operations.
//!
//! This module provides specialized utilities and simplified diff operations
//! for common operations.  It's useful when you want to work with text diffs
//! and you're interested in getting vectors of these changes directly.
//!
//! # Slice Remapping
//!
//! When working with [`TextDiff`] it's common that one takes advantage of the
//! built-in tokenization of the differ.  This for instance lets you do
//! grapheme level diffs.  This is implemented by the differ generating rather
//! small slices of strings and running a diff algorithm over them.
//!
//! The downside of this is that all the [`DiffOp`] objects produced by the
//! diffing algorithm encode operations on these rather small slices.  For
//! a lot of use cases this is not what one wants which can make this very
//! inconvenient.  This module provides a [`TextDiffRemapper`] which lets you
//! map from the ranges that the [`TextDiff`] returns to the original input
//! strings.  For more information see [`TextDiffRemapper`].
//!
//! # Simple Diff Functions
//!
//! This module provides a range of common test diff functions that will
//! produce vectors of `(change_tag, value)` tuples.  They will automatically
//! optimize towards returning the most useful slice that one would expect for
//! the type of diff performed.

use alloc::vec::Vec;
use core::hash::Hash;
use core::ops::{Index, Range};

use crate::{
    Algorithm, ChangeTag, DiffOp, DiffableStr, DiffableStrRef, TextDiff, capture_diff_slices,
};

#[cfg(feature = "inline")]
use crate::{InlineChange, InlineChangeOptions};

struct SliceRemapper<'x, T: ?Sized> {
    source: &'x T,
    indexes: Vec<Range<usize>>,
}

impl<'x, T: DiffableStr + ?Sized> SliceRemapper<'x, T> {
    fn from_lengths(
        source: &'x T,
        lengths: impl IntoIterator<Item = usize>,
    ) -> SliceRemapper<'x, T> {
        let mut offset = 0;
        let mut indexes = Vec::new();
        for len in lengths {
            let end = offset + len;
            indexes.push(offset..end);
            offset = end;
        }
        SliceRemapper { source, indexes }
    }

    fn new(source: &'x T, slices: &[&'x T]) -> SliceRemapper<'x, T> {
        Self::from_lengths(source, slices.iter().map(|item| item.len()))
    }

    fn slice(&self, range: Range<usize>) -> Option<&'x T> {
        let start = self.indexes.get(range.start)?.start;
        let end = self.indexes.get(range.end - 1)?.end;
        Some(self.source.slice(start..end))
    }
}

impl<T: DiffableStr + ?Sized> Index<Range<usize>> for SliceRemapper<'_, T> {
    type Output = T;

    fn index(&self, range: Range<usize>) -> &Self::Output {
        self.slice(range).expect("out of bounds")
    }
}

/// A remapper that can remap diff ops to the original slices.
///
/// The idea here is that when a [`TextDiff`] is created from
/// two strings and the internal tokenization is used, this remapper can take
/// a range in the tokenized sequences and remap it to the original string.
/// This is particularly useful when you want to do things like character or
/// grapheme level diffs but you want to not have to iterate over small sequences
/// but large consequitive ones from the source.
///
/// ```rust
/// use similar::{ChangeTag, TextDiff};
/// use similar::utils::TextDiffRemapper;
///
/// let old = "yo! foo bar baz";
/// let new = "yo! foo bor baz";
/// let diff = TextDiff::from_words(old, new);
/// let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
/// let mut changes = Vec::new();
/// for op in diff.ops() {
///     changes.extend(remapper.iter_slices(op));
/// }
///
/// assert_eq!(changes, vec![
///     (ChangeTag::Equal, "yo! foo "),
///     (ChangeTag::Delete, "bar"),
///     (ChangeTag::Insert, "bor"),
///     (ChangeTag::Equal, " baz")
/// ]);
pub struct TextDiffRemapper<'x, T: ?Sized> {
    old: SliceRemapper<'x, T>,
    new: SliceRemapper<'x, T>,
}

impl<'x, T: DiffableStr + ?Sized> TextDiffRemapper<'x, T> {
    /// Creates a new remapper from strings and slices.
    pub fn new(
        old_slices: &[&'x T],
        new_slices: &[&'x T],
        old: &'x T,
        new: &'x T,
    ) -> TextDiffRemapper<'x, T> {
        TextDiffRemapper {
            old: SliceRemapper::new(old, old_slices),
            new: SliceRemapper::new(new, new_slices),
        }
    }

    /// Creates a new remapper from a text diff and the original strings.
    pub fn from_text_diff<'old, 'new>(
        diff: &TextDiff<'old, 'new, T>,
        old: &'x T,
        new: &'x T,
    ) -> TextDiffRemapper<'x, T>
    where
        'old: 'x,
        'new: 'x,
    {
        TextDiffRemapper {
            old: SliceRemapper::from_lengths(
                old,
                (0..diff.old_len())
                    .map(|idx| diff.old_slice(idx).expect("slice out of bounds").len()),
            ),
            new: SliceRemapper::from_lengths(
                new,
                (0..diff.new_len())
                    .map(|idx| diff.new_slice(idx).expect("slice out of bounds").len()),
            ),
        }
    }

    /// Slices into the old string.
    pub fn slice_old(&self, range: Range<usize>) -> Option<&'x T> {
        self.old.slice(range)
    }

    /// Slices into the new string.
    pub fn slice_new(&self, range: Range<usize>) -> Option<&'x T> {
        self.new.slice(range)
    }

    /// Given a diffop yields the changes it encodes against the original strings.
    ///
    /// This is the same as the [`DiffOp::iter_slices`] method.
    ///
    /// ## Panics
    ///
    /// This method can panic if the input strings passed to the constructor
    /// are incompatible with the input strings passed to the diffing algorithm.
    pub fn iter_slices(
        &self,
        op: &DiffOp,
    ) -> impl Iterator<Item = (ChangeTag, &'x T)> + use<'x, T> {
        // note: this is equivalent to the code in `DiffOp::iter_slices`.  It is
        // a copy/paste because the slicing currently cannot be well abstracted
        // because of lifetime issues caused by the `Index` trait.
        match *op {
            DiffOp::Equal { old_index, len, .. } => {
                Some((ChangeTag::Equal, self.old.slice(old_index..old_index + len)))
                    .into_iter()
                    .chain(None)
            }
            DiffOp::Insert {
                new_index, new_len, ..
            } => Some((
                ChangeTag::Insert,
                self.new.slice(new_index..new_index + new_len),
            ))
            .into_iter()
            .chain(None),
            DiffOp::Delete {
                old_index, old_len, ..
            } => Some((
                ChangeTag::Delete,
                self.old.slice(old_index..old_index + old_len),
            ))
            .into_iter()
            .chain(None),
            DiffOp::Replace {
                old_index,
                old_len,
                new_index,
                new_len,
            } => Some((
                ChangeTag::Delete,
                self.old.slice(old_index..old_index + old_len),
            ))
            .into_iter()
            .chain(Some((
                ChangeTag::Insert,
                self.new.slice(new_index..new_index + new_len),
            ))),
        }
        .map(|(tag, opt_val)| (tag, opt_val.expect("slice out of bounds")))
    }
}

/// Shortcut for diffing two slices.
///
/// This function produces the diff of two slices and returns a vector
/// with the changes.
///
/// ```rust
/// use similar::{Algorithm, ChangeTag};
/// use similar::utils::diff_slices;
///
/// let old = "foo\nbar\nbaz".lines().collect::<Vec<_>>();
/// let new = "foo\nbar\nBAZ".lines().collect::<Vec<_>>();
/// assert_eq!(diff_slices(Algorithm::Myers, &old, &new), vec![
///     (ChangeTag::Equal, &["foo", "bar"][..]),
///     (ChangeTag::Delete, &["baz"][..]),
///     (ChangeTag::Insert, &["BAZ"][..]),
/// ]);
/// ```
pub fn diff_slices<'x, T: PartialEq + Hash + Ord>(
    alg: Algorithm,
    old: &'x [T],
    new: &'x [T],
) -> Vec<(ChangeTag, &'x [T])> {
    capture_diff_slices(alg, old, new)
        .iter()
        .flat_map(|op| op.iter_slices(old, new))
        .collect()
}

/// Shortcut for making a character level diff.
///
/// This function produces the diff of two strings and returns a vector
/// with the changes.  It returns connected slices into the original string
/// rather than character level slices.
///
/// ```rust
/// use similar::{Algorithm, ChangeTag};
/// use similar::utils::diff_chars;
///
/// assert_eq!(diff_chars(Algorithm::Myers, "foobarbaz", "fooBARbaz"), vec![
///     (ChangeTag::Equal, "foo"),
///     (ChangeTag::Delete, "bar"),
///     (ChangeTag::Insert, "BAR"),
///     (ChangeTag::Equal, "baz"),
/// ]);
/// ```
pub fn diff_chars<'x, T: DiffableStrRef + ?Sized>(
    alg: Algorithm,
    old: &'x T,
    new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
    let old = old.as_diffable_str();
    let new = new.as_diffable_str();
    let diff = TextDiff::configure().algorithm(alg).diff_chars(old, new);
    let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
    let mut rv = Vec::new();
    for op in diff.ops() {
        rv.extend(remapper.iter_slices(op));
    }
    rv
}

/// Shortcut for making a word level diff.
///
/// This function produces the diff of two strings and returns a vector
/// with the changes.  It returns connected slices into the original string
/// rather than word level slices.
///
/// ```rust
/// use similar::{Algorithm, ChangeTag};
/// use similar::utils::diff_words;
///
/// assert_eq!(diff_words(Algorithm::Myers, "foo bar baz", "foo bor baz"), vec![
///     (ChangeTag::Equal, "foo "),
///     (ChangeTag::Delete, "bar"),
///     (ChangeTag::Insert, "bor"),
///     (ChangeTag::Equal, " baz"),
/// ]);
/// ```
pub fn diff_words<'x, T: DiffableStrRef + ?Sized>(
    alg: Algorithm,
    old: &'x T,
    new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
    let old = old.as_diffable_str();
    let new = new.as_diffable_str();
    let diff = TextDiff::configure().algorithm(alg).diff_words(old, new);
    let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
    let mut rv = Vec::new();
    for op in diff.ops() {
        rv.extend(remapper.iter_slices(op));
    }
    rv
}

/// Shortcut for making a unicode word level diff.
///
/// This function produces the diff of two strings and returns a vector
/// with the changes.  It returns connected slices into the original string
/// rather than word level slices.
///
/// ```rust
/// use similar::{Algorithm, ChangeTag};
/// use similar::utils::diff_unicode_words;
///
/// let old = "The quick (\"brown\") fox can't jump 32.3 feet, right?";
/// let new = "The quick (\"brown\") fox can't jump 9.84 meters, right?";
/// assert_eq!(diff_unicode_words(Algorithm::Myers, old, new), vec![
///     (ChangeTag::Equal, "The quick (\"brown\") fox can\'t jump "),
///     (ChangeTag::Delete, "32.3"),
///     (ChangeTag::Insert, "9.84"),
///     (ChangeTag::Equal, " "),
///     (ChangeTag::Delete, "feet"),
///     (ChangeTag::Insert, "meters"),
///     (ChangeTag::Equal, ", right?")
/// ]);
/// ```
///
/// This requires the `unicode` feature.
#[cfg(feature = "unicode")]
pub fn diff_unicode_words<'x, T: DiffableStrRef + ?Sized>(
    alg: Algorithm,
    old: &'x T,
    new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
    let old = old.as_diffable_str();
    let new = new.as_diffable_str();
    let diff = TextDiff::configure()
        .algorithm(alg)
        .diff_unicode_words(old, new);
    let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
    let mut rv = Vec::new();
    for op in diff.ops() {
        rv.extend(remapper.iter_slices(op));
    }
    rv
}

/// Shortcut for making a grapheme level diff.
///
/// This function produces the diff of two strings and returns a vector
/// with the changes.  It returns connected slices into the original string
/// rather than grapheme level slices.
///
/// ```rust
/// use similar::{Algorithm, ChangeTag};
/// use similar::utils::diff_graphemes;
///
/// let old = "The flag of Austria is 🇦🇹";
/// let new = "The flag of Albania is 🇦🇱";
/// assert_eq!(diff_graphemes(Algorithm::Myers, old, new), vec![
///     (ChangeTag::Equal, "The flag of A"),
///     (ChangeTag::Delete, "ustr"),
///     (ChangeTag::Insert, "lban"),
///     (ChangeTag::Equal, "ia is "),
///     (ChangeTag::Delete, "🇦🇹"),
///     (ChangeTag::Insert, "🇦🇱"),
/// ]);
/// ```
///
/// This requires the `unicode` feature.
#[cfg(feature = "unicode")]
pub fn diff_graphemes<'x, T: DiffableStrRef + ?Sized>(
    alg: Algorithm,
    old: &'x T,
    new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
    let old = old.as_diffable_str();
    let new = new.as_diffable_str();
    let diff = TextDiff::configure()
        .algorithm(alg)
        .diff_graphemes(old, new);
    let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
    let mut rv = Vec::new();
    for op in diff.ops() {
        rv.extend(remapper.iter_slices(op));
    }
    rv
}

/// Shortcut for making a line diff.
///
/// This function produces the diff of two slices and returns a vector
/// with the changes.  Unlike [`diff_chars`] or [`diff_slices`] it returns a
/// change tag for each line.
///
/// ```rust
/// use similar::{Algorithm, ChangeTag};
/// use similar::utils::diff_lines;
///
/// assert_eq!(diff_lines(Algorithm::Myers, "foo\nbar\nbaz\nblah", "foo\nbar\nbaz\nblurgh"), vec![
///     (ChangeTag::Equal, "foo\n"),
///     (ChangeTag::Equal, "bar\n"),
///     (ChangeTag::Equal, "baz\n"),
///     (ChangeTag::Delete, "blah"),
///     (ChangeTag::Insert, "blurgh"),
/// ]);
/// ```
pub fn diff_lines<'x, T: DiffableStrRef + ?Sized>(
    alg: Algorithm,
    old: &'x T,
    new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
    let old = old.as_diffable_str();
    let new = new.as_diffable_str();
    let old_slices = old.tokenize_lines();
    let new_slices = new.tokenize_lines();
    capture_diff_slices(alg, &old_slices, &new_slices)
        .iter()
        .flat_map(|op| op.iter_changes(&old_slices, &new_slices))
        .map(|change| (change.tag(), change.value()))
        .collect()
}

/// Expands a line diff into [`InlineChange`] values.
///
/// This applies [`TextDiff::iter_all_inline_changes_with_options`] to the
/// passed diff object.
///
/// Requires the `inline` feature.
#[cfg(feature = "inline")]
pub fn diff_lines_inline<'diff, 'old, 'new, T: DiffableStr + ?Sized>(
    diff: &'diff TextDiff<'old, 'new, T>,
    options: InlineChangeOptions,
) -> Vec<InlineChange<'diff, T>> {
    diff.iter_all_inline_changes_with_options(options).collect()
}

#[test]
fn test_remapper() {
    let a = "foo bar baz";
    let words = a.tokenize_words();
    dbg!(&words);
    let remap = SliceRemapper::new(a, &words);
    assert_eq!(remap.slice(0..3), Some("foo bar"));
    assert_eq!(remap.slice(1..3), Some(" bar"));
    assert_eq!(remap.slice(0..1), Some("foo"));
    assert_eq!(remap.slice(0..5), Some("foo bar baz"));
    assert_eq!(remap.slice(0..6), None);
}
