use std::{fmt, ops::Index, slice::SliceIndex};

use unicode_width::UnicodeWidthChar;

#[derive(Debug, PartialEq, Default)]
pub struct Line {
    data: Vec<char>,
}

#[derive(Debug, PartialEq)]
pub struct Span<'a>(&'a [char]);

pub trait TextLike: Index<usize, Output = char> + ToString {
    type Iter<'a>: Iterator<Item = &'a char>
    where
        Self: 'a;
    type Span<'a>: TextLike + 'a
    where
        Self: 'a;

    fn display_width(&self, tab_width: usize) -> usize;
    fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_>;
    fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_>;

    fn iter(&self) -> Self::Iter<'_>;
}

// Line
impl Line {
    pub fn new() -> Self {
        Self { data: vec![] }
    }

    #[allow(clippy::should_implement_trait)]
    pub fn from_str<S: AsRef<str>>(s: S) -> Self {
        let chars = s.as_ref().chars();
        Self {
            data: Vec::from_iter(chars),
        }
    }

    pub fn as_span(&self) -> Span {
        Span(self.data.as_ref())
    }

    pub fn len(&self) -> usize {
        self.data.len()
    }

    pub fn is_empty(&self) -> bool {
        self.data.is_empty()
    }

    pub fn split_off(&mut self, at: usize) -> Line {
        let data = self.data.split_off(at);
        Line { data }
    }

    pub fn insert(&mut self, at: usize, ch: char) {
        self.data.insert(at, ch);
    }

    pub fn remove(&mut self, at: usize) {
        self.data.remove(at);
    }

    pub fn extend(&mut self, other: Line) {
        self.data.extend(other.data);
    }
}

impl IntoIterator for Line {
    type Item = char;
    type IntoIter = std::vec::IntoIter<char>;

    fn into_iter(self) -> Self::IntoIter {
        self.data.into_iter()
    }
}

impl TextLike for Line {
    type Span<'a> = Span<'a>;
    type Iter<'a> = std::slice::Iter<'a, char>;

    fn display_width(&self, tab_width: usize) -> usize {
        self.as_span().display_width(tab_width)
    }

    fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
        self.as_span().span(range)
    }

    fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> {
        self.as_span().skip_to_width(offset, tab_width)
    }

    fn iter(&self) -> Self::Iter<'_> {
        self.data.iter()
    }
}

impl Index<usize> for Line {
    type Output = char;

    fn index(&self, index: usize) -> &Self::Output {
        &self.data[index]
    }
}

impl fmt::Display for Line {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use fmt::Write;
        self.data.iter().try_for_each(|i| f.write_char(*i))
    }
}

// Span
impl<'s> TextLike for Span<'s> {
    type Iter<'a>
        = std::slice::Iter<'a, char>
    where
        's: 'a;
    type Span<'a>
        = Span<'s>
    where
        's: 'a;

    fn display_width(&self, tab_width: usize) -> usize {
        self.0.iter().fold(0, |pos, &ch| match ch {
            '\t' => (pos + tab_width) & !(tab_width - 1),
            _ => pos + ch.width().unwrap_or(1),
        })
    }

    fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
        Span(&self.0[range])
    }

    fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> {
        let mut index = 0;
        let mut pos = 0;
        for &ch in self.0.iter() {
            if pos >= offset {
                break;
            }
            match ch {
                '\t' => pos = (pos + tab_width) & !(tab_width - 1),
                _ => pos += ch.width().unwrap_or(1),
            }
            index += 1;
        }

        self.span(index..)
    }

    fn iter(&self) -> Self::Iter<'_> {
        self.0.iter()
    }
}

impl Index<usize> for Span<'_> {
    type Output = char;

    fn index(&self, index: usize) -> &Self::Output {
        &self.0[index]
    }
}

impl fmt::Display for Span<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use fmt::Write;
        self.0.iter().try_for_each(|i| f.write_char(*i))
    }
}

#[cfg(test)]
mod tests {
    use crate::line::{Span, TextLike};

    use super::Line;

    #[test]
    fn line_from_str() {
        // pure ASCII
        let text = "abc123\n\t xyz";
        let line = Line::from_str(text);
        assert_eq!(
            line.data,
            vec!['a', 'b', 'c', '1', '2', '3', '\n', '\t', ' ', 'x', 'y', 'z']
        );

        // cyrillic unicode
        let text = "це тест123";
        let line = Line::from_str(text);
        assert_eq!(
            line.data,
            vec!['ц', 'е', ' ', 'т', 'е', 'с', 'т', '1', '2', '3']
        );

        // japanese unicode
        let text = "1日本2";
        let line = Line::from_str(text);
        assert_eq!(line.data, vec!['1', '日', '本', '2']);
    }

    #[test]
    fn line_to_string() {
        let line = Line {
            data: vec!['a', 'b', 'c', 'т', 'е', 'с', 'т', '1', '2', '3', '\n'],
        };
        assert_eq!(line.to_string().as_str(), "abcтест123\n");
    }

    #[test]
    fn line_span() {
        // All span
        let line = Line::from_str("abcdef");
        assert_eq!(line.as_span(), Span(&['a', 'b', 'c', 'd', 'e', 'f']));

        assert_eq!(line.span(..3), Span(&['a', 'b', 'c']));
        assert_eq!(line.span(..=3), Span(&['a', 'b', 'c', 'd']));

        assert_eq!(line.span(..=3).span(2..), Span(&['c', 'd']));
        assert_eq!(line.span(2..=3), Span(&['c', 'd']));
    }

    #[test]
    fn line_width() {
        // No tabs
        let line = Line::from_str("abcdef");
        assert_eq!(line.display_width(4), line.len());

        // Tabs
        let line = Line::from_str("\ta\tbcdef");
        assert_eq!(line.display_width(4), 8 + 5);
    }
}
