//! Unicode-aware text width measurement and truncation.
//!
//! Terminal cells aren't 1:1 with bytes or even chars. CJK characters
//! occupy 2 cells, emoji ZWJ sequences are single grapheme clusters,
//! and combining marks have zero width. This module provides correct
//! measurement and truncation that never splits a grapheme cluster.

use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

/// Measure the display width of a string in terminal cells.
///
/// - ASCII characters: 1 cell each
/// - CJK characters: 2 cells each
/// - Emoji: varies (ZWJ sequences treated as grapheme clusters)
/// - Combining marks: 0 cells
#[must_use]
pub fn measure_display_width(s: &str) -> usize {
    UnicodeWidthStr::width(s)
}

/// Truncate a string to fit within `max_width` terminal cells.
///
/// Appends an ellipsis character if truncation occurs. Never splits
/// a grapheme cluster — if appending the next cluster would exceed
/// the limit, it stops before that cluster.
///
/// The ellipsis itself occupies 1 cell of the budget.
#[must_use]
pub fn truncate_display_width(s: &str, max_width: usize) -> String {
    let full_width = measure_display_width(s);
    if full_width <= max_width {
        return s.to_string();
    }

    if max_width == 0 {
        return String::new();
    }

    // Reserve 1 cell for the ellipsis.
    let budget = max_width.saturating_sub(1);
    let mut result = String::new();
    let mut used = 0;

    for grapheme in s.graphemes(true) {
        let gw = UnicodeWidthStr::width(grapheme);
        if used + gw > budget {
            break;
        }
        result.push_str(grapheme);
        used += gw;
    }

    result.push('\u{2026}'); // ellipsis
    result
}

/// Pad a string with trailing spaces to reach `width` terminal cells.
///
/// If the string is already wider than `width`, returns it unchanged.
#[must_use]
pub fn pad_display_width(s: &str, width: usize) -> String {
    let current = measure_display_width(s);
    if current >= width {
        return s.to_string();
    }
    let padding = width - current;
    let mut result = s.to_string();
    for _ in 0..padding {
        result.push(' ');
    }
    result
}

// ---------------------------------------------------------------------------
// Cursor / char-boundary helpers
// ---------------------------------------------------------------------------

/// Find the byte offset of the previous char boundary before `pos`.
///
/// Walks backwards from `pos - 1` until a valid char boundary is found.
/// Returns 0 if `pos` is 0 or 1.
pub(crate) fn prev_char_boundary(s: &str, pos: usize) -> usize {
    let mut i = pos.saturating_sub(1);
    while i > 0 && !s.is_char_boundary(i) {
        i -= 1;
    }
    i
}

/// Find the byte offset of the next char boundary after `pos`.
///
/// Walks forward from `pos + 1` until a valid char boundary is found.
/// Returns `s.len()` if already at or past the end.
pub(crate) fn next_char_boundary(s: &str, pos: usize) -> usize {
    let mut i = pos + 1;
    while i < s.len() && !s.is_char_boundary(i) {
        i += 1;
    }
    i
}

/// Convert a byte-offset cursor position to a display-column offset.
///
/// Snaps to the nearest char boundary at or before `cursor`, then counts
/// the number of characters from the start of the string to that point.
/// This gives the correct terminal column offset for cursor rendering.
pub(crate) fn cursor_cell_offset(text: &str, cursor: usize) -> u16 {
    let mut idx = cursor.min(text.len());
    while idx > 0 && !text.is_char_boundary(idx) {
        idx -= 1;
    }
    text[..idx].chars().count().min(u16::MAX as usize) as u16
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    // --- measure_display_width ---

    #[test]
    fn test_measure_ascii() {
        assert_eq!(measure_display_width("Hello"), 5);
    }

    #[test]
    fn test_measure_empty() {
        assert_eq!(measure_display_width(""), 0);
    }

    #[test]
    fn test_measure_cjk_width() {
        // TDD anchor from the bead spec
        assert_eq!(measure_display_width("Hello"), 5);
        assert_eq!(measure_display_width("\u{65E5}\u{672C}\u{8A9E}"), 6); // 日本語 = 3 chars * 2 cells
    }

    #[test]
    fn test_measure_mixed_ascii_cjk() {
        // "Hi日本" = 2 + 2 + 2 = 6
        assert_eq!(measure_display_width("Hi\u{65E5}\u{672C}"), 6);
    }

    #[test]
    fn test_measure_combining_marks() {
        // e + combining acute accent = 1 cell (combining mark is 0-width)
        assert_eq!(measure_display_width("e\u{0301}"), 1);
    }

    // --- truncate_display_width ---

    #[test]
    fn test_truncate_no_truncation_needed() {
        assert_eq!(truncate_display_width("Hello", 10), "Hello");
    }

    #[test]
    fn test_truncate_exact_fit() {
        assert_eq!(truncate_display_width("Hello", 5), "Hello");
    }

    #[test]
    fn test_truncate_ascii() {
        // "Hello World" is 11 cells. Truncate to 8: budget=7 for text + 1 for ellipsis
        let result = truncate_display_width("Hello World", 8);
        assert_eq!(measure_display_width(&result), 8); // 7 chars + ellipsis
        assert!(result.ends_with('\u{2026}'));
    }

    #[test]
    fn test_truncate_cjk_no_split() {
        // 日本語テスト = 6 chars * 2 cells = 12 cells
        // Truncate to 5: budget=4 for text + 1 for ellipsis
        // Can fit 2 CJK chars (4 cells), then ellipsis
        let result = truncate_display_width("\u{65E5}\u{672C}\u{8A9E}\u{30C6}\u{30B9}\u{30C8}", 5);
        assert!(result.ends_with('\u{2026}'));
        assert!(measure_display_width(&result) <= 5);
    }

    #[test]
    fn test_truncate_zero_width() {
        assert_eq!(truncate_display_width("Hello", 0), "");
    }

    #[test]
    fn test_truncate_width_one() {
        // Only room for the ellipsis itself
        let result = truncate_display_width("Hello", 1);
        assert_eq!(result, "\u{2026}");
    }

    #[test]
    fn test_truncate_emoji() {
        // Family emoji (ZWJ sequence) — should not be split
        let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"; // 👨‍👩‍👧
        let result = truncate_display_width(&format!("{family}Hello"), 3);
        // The emoji grapheme cluster is > 1 cell; if it doesn't fit in budget,
        // it should be skipped entirely, leaving just the ellipsis or less.
        assert!(measure_display_width(&result) <= 3);
    }

    // --- pad_display_width ---

    #[test]
    fn test_pad_basic() {
        let result = pad_display_width("Hi", 5);
        assert_eq!(result, "Hi   ");
        assert_eq!(measure_display_width(&result), 5);
    }

    #[test]
    fn test_pad_already_wide_enough() {
        assert_eq!(pad_display_width("Hello", 3), "Hello");
    }

    #[test]
    fn test_pad_exact_width() {
        assert_eq!(pad_display_width("Hello", 5), "Hello");
    }

    #[test]
    fn test_pad_cjk() {
        // 日本 = 4 cells, pad to 6 = 2 spaces
        let result = pad_display_width("\u{65E5}\u{672C}", 6);
        assert_eq!(measure_display_width(&result), 6);
        assert!(result.ends_with("  "));
    }

    // --- prev_char_boundary / next_char_boundary ---

    #[test]
    fn test_prev_char_boundary_ascii() {
        assert_eq!(prev_char_boundary("hello", 3), 2);
        assert_eq!(prev_char_boundary("hello", 1), 0);
    }

    #[test]
    fn test_prev_char_boundary_at_zero() {
        assert_eq!(prev_char_boundary("hello", 0), 0);
    }

    #[test]
    fn test_prev_char_boundary_multibyte() {
        // "aé" = 'a' (1 byte) + 'é' (2 bytes) = 3 bytes total
        let s = "a\u{00E9}b";
        // Position 3 = start of 'b', prev boundary = 1 (start of 'é')
        assert_eq!(prev_char_boundary(s, 3), 1);
        // Position 2 = mid-'é' byte, should snap to 1
        assert_eq!(prev_char_boundary(s, 2), 1);
    }

    #[test]
    fn test_next_char_boundary_ascii() {
        assert_eq!(next_char_boundary("hello", 0), 1);
        assert_eq!(next_char_boundary("hello", 3), 4);
    }

    #[test]
    fn test_next_char_boundary_multibyte() {
        // "aé" = 'a' (1 byte) + 'é' (2 bytes)
        let s = "a\u{00E9}b";
        // Position 1 = start of 'é', next boundary = 3 (start of 'b')
        assert_eq!(next_char_boundary(s, 1), 3);
    }

    #[test]
    fn test_next_char_boundary_at_end() {
        assert_eq!(next_char_boundary("hi", 2), 3);
    }

    // --- cursor_cell_offset ---

    #[test]
    fn test_cursor_cell_offset_ascii() {
        assert_eq!(cursor_cell_offset("hello", 0), 0);
        assert_eq!(cursor_cell_offset("hello", 3), 3);
        assert_eq!(cursor_cell_offset("hello", 5), 5);
    }

    #[test]
    fn test_cursor_cell_offset_multibyte() {
        // "aéb" = byte offsets: a=0, é=1..3, b=3
        let s = "a\u{00E9}b";
        assert_eq!(cursor_cell_offset(s, 0), 0); // before 'a'
        assert_eq!(cursor_cell_offset(s, 1), 1); // after 'a', before 'é'
        assert_eq!(cursor_cell_offset(s, 2), 1); // mid-'é', snaps back to 1
        assert_eq!(cursor_cell_offset(s, 3), 2); // after 'é', before 'b'
        assert_eq!(cursor_cell_offset(s, 4), 3); // after 'b'
    }

    #[test]
    fn test_cursor_cell_offset_beyond_end() {
        assert_eq!(cursor_cell_offset("hi", 99), 2);
    }
}
