use alloc::borrow::{Borrow, Cow, ToOwned};
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::hash::Hash;
use core::ops::Range;

/// Reference to a [`DiffableStr`].
///
/// This type exists because while the library only really provides ways to
/// work with `&str` and `&[u8]` there are types that deref into those string
/// slices such as `String` and `Vec<u8>`.
///
/// This trait is used in the library whenever it's nice to be able to pass
/// strings of different types in.
///
/// Requires the `text` feature.
pub trait DiffableStrRef {
    /// The type of the resolved [`DiffableStr`].
    type Output: DiffableStr + ?Sized;

    /// Resolves the reference.
    fn as_diffable_str(&self) -> &Self::Output;
}

impl<T: DiffableStr + ?Sized> DiffableStrRef for T {
    type Output = T;

    fn as_diffable_str(&self) -> &T {
        self
    }
}

impl DiffableStrRef for String {
    type Output = str;

    fn as_diffable_str(&self) -> &str {
        self.as_str()
    }
}

impl<T: DiffableStr + ?Sized> DiffableStrRef for Cow<'_, T> {
    type Output = T;

    fn as_diffable_str(&self) -> &T {
        self
    }
}

/// Input type for text diffing.
///
/// This enum allows the text diffing APIs to accept both borrowed and owned
/// input values.
pub enum DiffInput<'a, T: DiffableStr + ?Sized> {
    /// A borrowed input value.
    Borrowed(&'a T),
    /// An owned input value.
    Owned(<T as ToOwned>::Owned),
}

impl<T: DiffableStr + ?Sized> DiffInput<'_, T> {
    /// Returns the input as [`DiffableStr`] reference.
    pub fn as_diffable_str(&self) -> &T {
        match self {
            DiffInput::Borrowed(value) => value,
            DiffInput::Owned(value) => value.borrow(),
        }
    }
}

/// Converts supported text values into [`DiffInput`].
///
/// This trait is implemented for both borrowed and owned input types used by
/// text diff APIs.
pub trait IntoDiffInput<'a> {
    /// The resolved [`DiffableStr`] output type.
    type Output: DiffableStr + ?Sized;

    /// Converts the input value into a [`DiffInput`].
    fn into_diff_input(self) -> DiffInput<'a, Self::Output>;
}

impl<'a, T: DiffableStrRef + ?Sized> IntoDiffInput<'a> for &'a T {
    type Output = T::Output;

    fn into_diff_input(self) -> DiffInput<'a, Self::Output> {
        DiffInput::Borrowed(self.as_diffable_str())
    }
}

impl<'a> IntoDiffInput<'a> for String {
    type Output = str;

    fn into_diff_input(self) -> DiffInput<'a, Self::Output> {
        DiffInput::Owned(self)
    }
}

impl<'a> IntoDiffInput<'a> for Cow<'a, str> {
    type Output = str;

    fn into_diff_input(self) -> DiffInput<'a, Self::Output> {
        match self {
            Cow::Borrowed(value) => DiffInput::Borrowed(value),
            Cow::Owned(value) => DiffInput::Owned(value),
        }
    }
}

/// All supported diffable strings.
///
/// The text module can work with different types of strings depending
/// on how the crate is compiled.  Out of the box `&str` is always supported
/// but with the `bytes` feature one can also work with `[u8]` slices for
/// as long as they are ASCII compatible.
///
/// Requires the `text` feature.
pub trait DiffableStr: Hash + PartialEq + PartialOrd + Ord + Eq + ToOwned {
    /// Splits the value into newlines with newlines attached.
    fn tokenize_lines(&self) -> Vec<&Self>;

    /// Splits the value into newlines with newlines separated.
    fn tokenize_lines_and_newlines(&self) -> Vec<&Self>;

    /// Tokenizes into words.
    fn tokenize_words(&self) -> Vec<&Self>;

    /// Tokenizes the input into characters.
    fn tokenize_chars(&self) -> Vec<&Self>;

    /// Tokenizes into unicode words.
    #[cfg(feature = "unicode")]
    fn tokenize_unicode_words(&self) -> Vec<&Self>;

    /// Tokenizes into unicode graphemes.
    #[cfg(feature = "unicode")]
    fn tokenize_graphemes(&self) -> Vec<&Self>;

    /// Decodes the string (potentially) lossy.
    fn as_str(&self) -> Option<&str>;

    /// Decodes the string (potentially) lossy.
    fn to_string_lossy(&self) -> Cow<'_, str>;

    /// Checks if the string ends in a newline.
    fn ends_with_newline(&self) -> bool;

    /// The length of the string.
    fn len(&self) -> usize;

    /// Slices the string.
    fn slice(&self, rng: Range<usize>) -> &Self;

    /// Returns the string as slice of raw bytes.
    fn as_bytes(&self) -> &[u8];

    /// Checks if the string is empty.
    fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

impl DiffableStr for str {
    fn tokenize_lines(&self) -> Vec<&Self> {
        let mut iter = self.char_indices().peekable();
        let mut last_pos = 0;
        let mut lines = vec![];

        while let Some((idx, c)) = iter.next() {
            if c == '\r' {
                if iter.peek().is_some_and(|x| x.1 == '\n') {
                    lines.push(&self[last_pos..=idx + 1]);
                    iter.next();
                    last_pos = idx + 2;
                } else {
                    lines.push(&self[last_pos..=idx]);
                    last_pos = idx + 1;
                }
            } else if c == '\n' {
                lines.push(&self[last_pos..=idx]);
                last_pos = idx + 1;
            }
        }

        if last_pos < self.len() {
            lines.push(&self[last_pos..]);
        }

        lines
    }

    fn tokenize_lines_and_newlines(&self) -> Vec<&Self> {
        let mut rv = vec![];
        let mut iter = self.char_indices().peekable();

        while let Some((idx, c)) = iter.next() {
            let is_newline = c == '\r' || c == '\n';
            let start = idx;
            let mut end = idx + c.len_utf8();
            while let Some(&(_, next_char)) = iter.peek() {
                if (next_char == '\r' || next_char == '\n') != is_newline {
                    break;
                }
                iter.next();
                end += next_char.len_utf8();
            }
            rv.push(&self[start..end]);
        }

        rv
    }

    fn tokenize_words(&self) -> Vec<&Self> {
        let mut iter = self.char_indices().peekable();
        let mut rv = vec![];

        while let Some((idx, c)) = iter.next() {
            let is_whitespace = c.is_whitespace();
            let start = idx;
            let mut end = idx + c.len_utf8();
            while let Some(&(_, next_char)) = iter.peek() {
                if next_char.is_whitespace() != is_whitespace {
                    break;
                }
                iter.next();
                end += next_char.len_utf8();
            }
            rv.push(&self[start..end]);
        }

        rv
    }

    fn tokenize_chars(&self) -> Vec<&Self> {
        self.char_indices()
            .map(move |(i, c)| &self[i..i + c.len_utf8()])
            .collect()
    }

    #[cfg(feature = "unicode")]
    fn tokenize_unicode_words(&self) -> Vec<&Self> {
        unicode_segmentation::UnicodeSegmentation::split_word_bounds(self).collect()
    }

    #[cfg(feature = "unicode")]
    fn tokenize_graphemes(&self) -> Vec<&Self> {
        unicode_segmentation::UnicodeSegmentation::graphemes(self, true).collect()
    }

    fn as_str(&self) -> Option<&str> {
        Some(self)
    }

    fn to_string_lossy(&self) -> Cow<'_, str> {
        Cow::Borrowed(self)
    }

    fn ends_with_newline(&self) -> bool {
        self.ends_with(&['\r', '\n'][..])
    }

    fn len(&self) -> usize {
        str::len(self)
    }

    fn slice(&self, rng: Range<usize>) -> &Self {
        &self[rng]
    }

    fn as_bytes(&self) -> &[u8] {
        str::as_bytes(self)
    }
}

#[cfg(feature = "bytes")]
mod bytes_support {
    use super::*;

    use alloc::vec;
    use bstr::ByteSlice;

    impl DiffableStrRef for Vec<u8> {
        type Output = [u8];

        fn as_diffable_str(&self) -> &[u8] {
            self.as_slice()
        }
    }

    impl<'a> IntoDiffInput<'a> for Vec<u8> {
        type Output = [u8];

        fn into_diff_input(self) -> DiffInput<'a, Self::Output> {
            DiffInput::Owned(self)
        }
    }

    impl<'a> IntoDiffInput<'a> for Cow<'a, [u8]> {
        type Output = [u8];

        fn into_diff_input(self) -> DiffInput<'a, Self::Output> {
            match self {
                Cow::Borrowed(value) => DiffInput::Borrowed(value),
                Cow::Owned(value) => DiffInput::Owned(value),
            }
        }
    }

    /// Allows viewing ASCII compatible byte slices as strings.
    ///
    /// Requires the `bytes` feature.
    impl DiffableStr for [u8] {
        fn tokenize_lines(&self) -> Vec<&Self> {
            let mut iter = self.char_indices().peekable();
            let mut last_pos = 0;
            let mut lines = vec![];

            while let Some((_, end, c)) = iter.next() {
                if c == '\r' {
                    if iter.peek().is_some_and(|x| x.2 == '\n') {
                        lines.push(&self[last_pos..end + 1]);
                        iter.next();
                        last_pos = end + 1;
                    } else {
                        lines.push(&self[last_pos..end]);
                        last_pos = end;
                    }
                } else if c == '\n' {
                    lines.push(&self[last_pos..end]);
                    last_pos = end;
                }
            }

            if last_pos < self.len() {
                lines.push(&self[last_pos..]);
            }

            lines
        }

        fn tokenize_lines_and_newlines(&self) -> Vec<&Self> {
            let mut rv = vec![];
            let mut iter = self.char_indices().peekable();

            while let Some((start, mut end, c)) = iter.next() {
                let is_newline = c == '\r' || c == '\n';
                while let Some(&(_, new_end, next_char)) = iter.peek() {
                    if (next_char == '\r' || next_char == '\n') != is_newline {
                        break;
                    }
                    iter.next();
                    end = new_end;
                }
                rv.push(&self[start..end]);
            }

            rv
        }

        fn tokenize_words(&self) -> Vec<&Self> {
            let mut iter = self.char_indices().peekable();
            let mut rv = vec![];

            while let Some((start, mut end, c)) = iter.next() {
                let is_whitespace = c.is_whitespace();
                while let Some(&(_, new_end, next_char)) = iter.peek() {
                    if next_char.is_whitespace() != is_whitespace {
                        break;
                    }
                    iter.next();
                    end = new_end;
                }
                rv.push(&self[start..end]);
            }

            rv
        }

        #[cfg(feature = "unicode")]
        fn tokenize_unicode_words(&self) -> Vec<&Self> {
            self.words_with_breaks().map(|x| x.as_bytes()).collect()
        }

        #[cfg(feature = "unicode")]
        fn tokenize_graphemes(&self) -> Vec<&Self> {
            self.graphemes().map(|x| x.as_bytes()).collect()
        }

        fn tokenize_chars(&self) -> Vec<&Self> {
            self.char_indices()
                .map(move |(start, end, _)| &self[start..end])
                .collect()
        }

        fn as_str(&self) -> Option<&str> {
            core::str::from_utf8(self).ok()
        }

        fn to_string_lossy(&self) -> Cow<'_, str> {
            String::from_utf8_lossy(self)
        }

        fn ends_with_newline(&self) -> bool {
            matches!(self.last_byte(), Some(b'\r') | Some(b'\n'))
        }

        fn len(&self) -> usize {
            <[u8]>::len(self)
        }

        fn slice(&self, rng: Range<usize>) -> &Self {
            &self[rng]
        }

        fn as_bytes(&self) -> &[u8] {
            self
        }
    }
}

#[test]
fn test_split_lines() {
    assert_eq!(
        DiffableStr::tokenize_lines("first\nsecond\rthird\r\nfourth\nlast"),
        vec!["first\n", "second\r", "third\r\n", "fourth\n", "last"]
    );
    assert_eq!(DiffableStr::tokenize_lines("\n\n"), vec!["\n", "\n"]);
    assert_eq!(DiffableStr::tokenize_lines("\n"), vec!["\n"]);
    assert!(DiffableStr::tokenize_lines("").is_empty());
}

#[test]
fn test_split_words() {
    assert_eq!(
        DiffableStr::tokenize_words("foo    bar baz\n\n  aha"),
        ["foo", "    ", "bar", " ", "baz", "\n\n  ", "aha"]
    );
}

#[test]
fn test_split_chars() {
    assert_eq!(
        DiffableStr::tokenize_chars("abcfö❄️"),
        vec!["a", "b", "c", "f", "ö", "❄", "\u{fe0f}"]
    );
}

#[test]
#[cfg(feature = "unicode")]
fn test_split_graphemes() {
    assert_eq!(
        DiffableStr::tokenize_graphemes("abcfö❄️"),
        vec!["a", "b", "c", "f", "ö", "❄️"]
    );
}

#[test]
#[cfg(feature = "bytes")]
fn test_split_lines_bytes() {
    assert_eq!(
        DiffableStr::tokenize_lines("first\nsecond\rthird\r\nfourth\nlast".as_bytes()),
        vec![
            "first\n".as_bytes(),
            "second\r".as_bytes(),
            "third\r\n".as_bytes(),
            "fourth\n".as_bytes(),
            "last".as_bytes()
        ]
    );
    assert_eq!(
        DiffableStr::tokenize_lines("\n\n".as_bytes()),
        vec!["\n".as_bytes(), "\n".as_bytes()]
    );
    assert_eq!(
        DiffableStr::tokenize_lines("\n".as_bytes()),
        vec!["\n".as_bytes()]
    );
    assert!(DiffableStr::tokenize_lines("".as_bytes()).is_empty());
}

#[test]
#[cfg(feature = "bytes")]
fn test_split_words_bytes() {
    assert_eq!(
        DiffableStr::tokenize_words("foo    bar baz\n\n  aha".as_bytes()),
        [
            &b"foo"[..],
            &b"    "[..],
            &b"bar"[..],
            &b" "[..],
            &b"baz"[..],
            &b"\n\n  "[..],
            &b"aha"[..]
        ]
    );
}

#[test]
#[cfg(feature = "bytes")]
fn test_split_chars_bytes() {
    assert_eq!(
        DiffableStr::tokenize_chars("abcfö❄️".as_bytes()),
        vec![
            &b"a"[..],
            &b"b"[..],
            &b"c"[..],
            &b"f"[..],
            "ö".as_bytes(),
            "❄".as_bytes(),
            "\u{fe0f}".as_bytes()
        ]
    );
}

#[test]
#[cfg(all(feature = "bytes", feature = "unicode"))]
fn test_split_graphemes_bytes() {
    assert_eq!(
        DiffableStr::tokenize_graphemes("abcfö❄️".as_bytes()),
        vec![
            &b"a"[..],
            &b"b"[..],
            &b"c"[..],
            &b"f"[..],
            "ö".as_bytes(),
            "❄️".as_bytes()
        ]
    );
}
