use core::fmt;

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct TextMetrics {
    pub bytes: usize,
    pub chars: usize,
    pub utf16: usize,
    pub line_breaks: usize,
}

impl TextMetrics {
    pub fn from_text(text: &str) -> Self {
        let mut metrics = Self::default();
        for ch in text.chars() {
            metrics.bytes += ch.len_utf8();
            metrics.chars += 1;
            metrics.utf16 += ch.len_utf16();
            metrics.line_breaks += usize::from(ch == '\n');
        }
        metrics
    }
}

impl core::ops::Add for TextMetrics {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self {
            bytes: self.bytes + rhs.bytes,
            chars: self.chars + rhs.chars,
            utf16: self.utf16 + rhs.utf16,
            line_breaks: self.line_breaks + rhs.line_breaks,
        }
    }
}

impl core::ops::AddAssign for TextMetrics {
    fn add_assign(&mut self, rhs: Self) {
        *self = *self + rhs;
    }
}

impl core::ops::Sub for TextMetrics {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        Self {
            bytes: self.bytes - rhs.bytes,
            chars: self.chars - rhs.chars,
            utf16: self.utf16 - rhs.utf16,
            line_breaks: self.line_breaks - rhs.line_breaks,
        }
    }
}

#[derive(Clone, Copy, Default, Eq, PartialEq)]
pub struct TextPoint {
    pub byte: usize,
    pub char_offset: usize,
    pub utf16_offset: usize,
    pub line: usize,
    pub column: usize,
}

impl TextPoint {
    pub const fn origin() -> Self {
        Self {
            byte: 0,
            char_offset: 0,
            utf16_offset: 0,
            line: 0,
            column: 0,
        }
    }

    pub fn advance(mut self, text: &str) -> Self {
        self.byte += text.len();
        for ch in text.chars() {
            self.char_offset += 1;
            self.utf16_offset += ch.len_utf16();
            if ch == '\n' {
                self.line += 1;
                self.column = 0;
            } else {
                self.column += 1;
            }
        }
        self
    }
}

impl fmt::Debug for TextPoint {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("TextPoint")
            .field("byte", &self.byte)
            .field("char_offset", &self.char_offset)
            .field("utf16_offset", &self.utf16_offset)
            .field("line", &self.line)
            .field("column", &self.column)
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::{TextMetrics, TextPoint};

    #[test]
    fn metrics_count_utf8_utf16_and_lines() {
        let metrics = TextMetrics::from_text("a\né🙂");
        assert_eq!(metrics.bytes, "a\né🙂".len());
        assert_eq!(metrics.chars, 4);
        assert_eq!(metrics.utf16, 5);
        assert_eq!(metrics.line_breaks, 1);
    }

    #[test]
    fn metrics_support_arithmetic() {
        let left = TextMetrics::from_text("hello\n");
        let right = TextMetrics::from_text("🙂");
        let combined = left + right;

        assert_eq!(combined.bytes, "hello\n🙂".len());
        assert_eq!(combined.chars, 7);
        assert_eq!(combined.utf16, 8);
        assert_eq!(combined.line_breaks, 1);
        assert_eq!(combined - right, left);
    }

    #[test]
    fn point_advance_tracks_offsets() {
        let point = TextPoint::origin().advance("ab\n🙂");
        assert_eq!(point.byte, "ab\n🙂".len());
        assert_eq!(point.char_offset, 4);
        assert_eq!(point.utf16_offset, 5);
        assert_eq!(point.line, 1);
        assert_eq!(point.column, 1);
    }
}
