//! Contents of a terminal screen. A [Buffer] is made up of [Cell]s.
use crate::text::{Span, Spans};
use helix_core::unicode::width::{UnicodeWidthChar, UnicodeWidthStr};
use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle};
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;

pub const SYMBOL_CAPACITY: usize = 28;
pub type Symbol = arrayvec::ArrayString<SYMBOL_CAPACITY>;

/// One cell of the terminal. Contains one stylized grapheme.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
    pub symbol: Symbol,
    /// Cached display width of `symbol` to avoid multiple recomputes in the hot path.
    width: u8,
    pub fg: Color,
    pub bg: Color,
    pub underline_color: Color,
    pub underline_style: UnderlineStyle,
    pub modifier: Modifier,
}

/// Char when attempting to set symbol that exceeds capacity: �
const REPLACEMENT_CHARACTER: char = '\u{FFFD}';

impl Cell {
    /// Set the cell's grapheme
    pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
        self.symbol.clear();
        if self.symbol.try_push_str(symbol).is_err() {
            debug_assert!(
                symbol.len() > SYMBOL_CAPACITY,
                "Symbols can't exceed {SYMBOL_CAPACITY} bytes.\nTried to push {symbol} (size in bytes: {})",
                symbol.len()
            );
            self.symbol.push(REPLACEMENT_CHARACTER);
        }
        self.width = self.symbol.width() as u8;
        self
    }

    /// Like `set_symbol`, but the caller has already computed the display width.
    /// Used on the render hot path where the width is known.
    pub(crate) fn set_symbol_with_width(&mut self, symbol: &str, width: u8) -> &mut Cell {
        self.symbol.clear();
        if self.symbol.try_push_str(symbol).is_err() {
            debug_assert!(
                symbol.len() > 28,
                "Symbols can't exceed 28 bytes.\nTried to push {} (size in bytes: {})",
                symbol,
                symbol.len()
            );
            self.symbol.push(REPLACEMENT_CHARACTER);
            self.width = REPLACEMENT_CHARACTER.width().unwrap_or(1) as u8;
        } else {
            self.width = width;
        }
        self
    }

    /// Display width (in terminal columns) of the cell's grapheme.
    pub fn width(&self) -> usize {
        self.width as usize
    }

    /// Set the cell's grapheme to a [char]
    pub fn set_char(&mut self, ch: char) -> &mut Cell {
        self.symbol.clear();
        self.symbol.push(ch);
        self.width = ch.width().unwrap_or(0) as u8;
        self
    }

    /// Set the foreground [Color]
    pub fn set_fg(&mut self, color: Color) -> &mut Cell {
        self.fg = color;
        self
    }

    /// Set the background [Color]
    pub fn set_bg(&mut self, color: Color) -> &mut Cell {
        self.bg = color;
        self
    }

    /// Set the [Style] of the cell
    pub fn set_style(&mut self, style: Style) -> &mut Cell {
        if let Some(c) = style.fg {
            self.fg = c;
        }
        if let Some(c) = style.bg {
            self.bg = c;
        }
        if let Some(c) = style.underline_color {
            self.underline_color = c;
        }
        if let Some(style) = style.underline_style {
            self.underline_style = style;
        }

        self.modifier.insert(style.add_modifier);
        self.modifier.remove(style.sub_modifier);
        self
    }

    /// Returns the current style of the cell
    pub fn style(&self) -> Style {
        Style::default()
            .fg(self.fg)
            .bg(self.bg)
            .underline_color(self.underline_color)
            .underline_style(self.underline_style)
            .add_modifier(self.modifier)
    }

    /// Resets the cell to a default blank state
    pub fn reset(&mut self) {
        *self = Self::default();
    }
}

impl Default for Cell {
    fn default() -> Cell {
        Cell {
            symbol: Symbol::from(" ").unwrap(),
            width: 1,
            fg: Color::Reset,
            bg: Color::Reset,
            underline_color: Color::Reset,
            underline_style: UnderlineStyle::Reset,
            modifier: Modifier::empty(),
        }
    }
}

/// A buffer that maps to the desired content of the terminal after the draw call
///
/// No widget in the library interacts directly with the terminal. Instead each of them is required
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
/// a grapheme, a foreground color and a background color. This grid will then be used to output
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
///
/// # Examples:
///
/// ```
/// use helix_tui::buffer::{Buffer, Cell, Symbol};
/// use helix_view::graphics::{Rect, Color, UnderlineStyle, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf[(0, 2)].set_symbol("x");
/// assert_eq!(&*buf[(0, 2)].symbol, "x");
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
/// let mut expected = Cell::default();
/// expected.set_symbol("r");
/// expected.set_style(Style::default().fg(Color::Red).bg(Color::White));
/// assert_eq!(buf[(5, 0)], expected);
/// buf[(5, 0)].set_char('x');
/// assert_eq!(&*buf[(5, 0)].symbol, "x");
/// ```
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Buffer {
    /// The area represented by this buffer
    pub area: Rect,
    /// The content of the buffer. The length of this Vec should always be equal to area.width *
    /// area.height
    pub content: Vec<Cell>,
}

impl Buffer {
    /// Returns a Buffer with all cells set to the default one
    #[must_use]
    pub fn empty(area: Rect) -> Buffer {
        Buffer::filled(area, &Cell::default())
    }

    /// Returns a Buffer with all cells initialized with the attributes of the given Cell
    #[must_use]
    pub fn filled(area: Rect, cell: &Cell) -> Buffer {
        let size = area.area();
        let content = vec![cell.clone(); size];
        Buffer { area, content }
    }

    /// Returns a Buffer containing the given lines
    #[must_use]
    pub fn with_lines<S>(lines: Vec<S>) -> Buffer
    where
        S: AsRef<str>,
    {
        let height = lines.len() as u16;
        let width = lines
            .iter()
            .map(|i| i.as_ref().width() as u16)
            .max()
            .unwrap_or_default();
        let mut buffer = Buffer::empty(Rect {
            x: 0,
            y: 0,
            width,
            height,
        });
        for (y, line) in lines.iter().enumerate() {
            buffer.set_string(0, y as u16, line.as_ref(), Style::default());
        }
        buffer
    }

    /// Returns the content of the buffer as a slice
    pub fn content(&self) -> &[Cell] {
        &self.content
    }

    /// Returns the area covered by this buffer
    pub fn area(&self) -> &Rect {
        &self.area
    }

    /// Returns a reference to Cell at the given coordinates
    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
        self.index_of_opt(x, y).map(|i| &self.content[i])
    }

    /// Returns a mutable reference to Cell at the given coordinates
    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
        self.index_of_opt(x, y).map(|i| &mut self.content[i])
    }

    /// Tells whether the global (x, y) coordinates are inside the Buffer's area.
    ///
    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
    ///
    /// # Examples
    ///
    /// ```
    /// # use helix_tui::buffer::Buffer;
    /// # use helix_view::graphics::Rect;
    /// let rect = Rect::new(200, 100, 10, 10);
    /// let buffer = Buffer::empty(rect);
    /// // Global coordinates inside the Buffer's area
    /// assert!(buffer.in_bounds(209, 100));
    /// // Global coordinates outside the Buffer's area
    /// assert!(!buffer.in_bounds(210, 100));
    /// ```
    ///
    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
    pub fn in_bounds(&self, x: u16, y: u16) -> bool {
        x >= self.area.left()
            && x < self.area.right()
            && y >= self.area.top()
            && y < self.area.bottom()
    }

    /// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
    ///
    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
    ///
    /// # Examples
    ///
    /// ```
    /// # use helix_tui::buffer::Buffer;
    /// # use helix_view::graphics::Rect;
    /// let rect = Rect::new(200, 100, 10, 10);
    /// let buffer = Buffer::empty(rect);
    /// // Global coordinates to the top corner of this Buffer's area
    /// assert_eq!(buffer.index_of(200, 100), 0);
    /// ```
    ///
    /// # Panics
    ///
    /// Panics when given an coordinate that is outside of this Buffer's area.
    pub fn index_of(&self, x: u16, y: u16) -> usize {
        debug_assert!(
            self.in_bounds(x, y),
            "Trying to access position outside the buffer: x={}, y={}, area={:?}",
            x,
            y,
            self.area
        );
        ((y - self.area.y) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize)
    }

    /// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates,
    /// or `None` if the coordinates are outside the buffer's area.
    fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> {
        if self.in_bounds(x, y) {
            Some(self.index_of(x, y))
        } else {
            None
        }
    }

    /// Returns the (global) coordinates of a cell given its index
    ///
    /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
    ///
    /// # Examples
    ///
    /// ```
    /// # use helix_tui::buffer::Buffer;
    /// # use helix_view::graphics::Rect;
    /// let rect = Rect::new(200, 100, 10, 10);
    /// let buffer = Buffer::empty(rect);
    /// assert_eq!(buffer.pos_of(0), (200, 100));
    /// assert_eq!(buffer.pos_of(14), (204, 101));
    /// ```
    ///
    /// # Panics
    ///
    /// Panics when given an index that is outside the Buffer's content.
    pub fn pos_of(&self, i: usize) -> (u16, u16) {
        debug_assert!(
            i < self.content.len(),
            "Trying to get the coords of a cell outside the buffer: i={} len={}",
            i,
            self.content.len()
        );
        (
            (self.area.x as usize + (i % self.area.width as usize)) as u16,
            (self.area.y as usize + (i / self.area.width as usize)) as u16,
        )
    }

    /// Print a string, starting at the position (x, y)
    pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: Style) {
        self.set_stringn(x, y, string, usize::MAX, style);
    }

    /// Print at most the first `width` columns of `string`, starting at (x, y),
    /// stopping at the end of the buffer line. Returns the position right after
    /// the last cell written.
    pub fn set_stringn(
        &mut self,
        x: u16,
        y: u16,
        string: &str,
        width: usize,
        style: Style,
    ) -> (u16, u16) {
        // prevent panic if out of range
        if !self.in_bounds(x, y) {
            return (x, y);
        }

        let mut index = self.index_of(x, y);
        let mut x_offset = x as usize;
        let max_x_offset = min(self.area.right() as usize, width.saturating_add(x as usize));

        for s in string.graphemes(true) {
            let grapheme_width = s.width();
            if grapheme_width == 0 {
                continue;
            }
            // `x_offset + grapheme_width > max_x_offset` could be integer overflow on
            // 32-bit machines if we change dimensions to usize or u32 and someone
            // resizes the terminal to 1x2^32.
            if grapheme_width > max_x_offset.saturating_sub(x_offset) {
                break;
            }

            self.content[index].set_symbol(s);
            self.content[index].set_style(style);
            // Reset following cells if multi-width (they would be hidden by the grapheme),
            for i in index + 1..index + grapheme_width {
                self.content[i].reset();
            }
            index += grapheme_width;
            x_offset += grapheme_width;
        }

        (x_offset as u16, y)
    }

    /// Fast path: print a single grapheme of pre-computed width with no unicode
    /// segmentation / width recomputation. Caller must guarantee the grapheme +
    /// width fit inside the buffer area and that `width >= 1`.
    ///
    /// This is the per-cell render path; it is hot enough that we skip everything
    /// `set_stringn` does (segmenting, calling `.width()`, bounds-checking each
    /// grapheme).
    #[inline]
    pub fn set_grapheme(&mut self, x: u16, y: u16, grapheme: &str, width: usize, style: Style) {
        let index = self.index_of(x, y);
        let cell = &mut self.content[index];
        cell.set_symbol_with_width(grapheme, width as u8);
        cell.set_style(style);
        // Reset following cells if the grapheme spans multiple columns; they
        // would be hidden by the grapheme but must not carry stale content.
        for i in index + 1..index + width {
            self.content[i].reset();
        }
    }

    /// Print at most the first `width` characters of a string if enough space is available
    /// until the end of the line.
    /// If `ellipsis` is true appends a `…` at the end of truncated lines.
    /// If `truncate_start` is `true`, adds a `…` at the beginning of truncated lines.
    #[allow(clippy::too_many_arguments)]
    pub fn set_string_anchored(
        &mut self,
        x: u16,
        y: u16,
        truncate_start: bool,
        truncate_end: bool,
        string: &str,
        width: usize,
        style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style
    ) -> (u16, u16) {
        // prevent panic if out of range
        if !self.in_bounds(x, y) || width == 0 {
            return (x, y);
        }

        let mut index = self.index_of(x, y);
        let mut rendered_width = 0;
        let mut graphemes = string.grapheme_indices(true);

        if truncate_start {
            for _ in 0..graphemes.next().map(|(_, g)| g.width()).unwrap_or_default() {
                self.content[index].set_symbol("…");
                index += 1;
                rendered_width += 1;
            }
        }

        for (byte_offset, s) in graphemes {
            let grapheme_width = s.width();
            if truncate_end && rendered_width + grapheme_width >= width {
                break;
            }
            if grapheme_width == 0 {
                continue;
            }

            self.content[index].set_symbol(s);
            self.content[index].set_style(style(byte_offset));

            // Reset following cells if multi-width (they would be hidden by the grapheme):
            for i in index + 1..index + grapheme_width {
                self.content[i].reset();
            }

            index += grapheme_width;
            rendered_width += grapheme_width;
        }

        if truncate_end {
            for _ in 0..width.saturating_sub(rendered_width) {
                self.content[index].set_symbol("…");
                index += 1;
            }
        }

        (x, y)
    }

    /// Print at most the first `width` characters of a string if enough space is available
    /// until the end of the line. If `ellipsis` is true appends a `…` at the end of
    /// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
    /// instead of the end.
    #[allow(clippy::too_many_arguments)]
    pub fn set_string_truncated(
        &mut self,
        x: u16,
        y: u16,
        string: &str,
        width: usize,
        style: impl Fn(usize) -> Style, // Map a grapheme's string offset to a style
        ellipsis: bool,
        truncate_start: bool,
    ) -> (u16, u16) {
        // prevent panic if out of range
        if !self.in_bounds(x, y) || width == 0 {
            return (x, y);
        }

        let mut index = self.index_of(x, y);
        let mut x_offset = x as usize;
        let width = if ellipsis { width - 1 } else { width };
        let graphemes = string.grapheme_indices(true);
        let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
        if !truncate_start {
            for (byte_offset, s) in graphemes {
                let width = s.width();
                if width == 0 {
                    continue;
                }
                // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
                // change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
                if width > max_offset.saturating_sub(x_offset) {
                    break;
                }

                self.content[index].set_symbol(s);
                self.content[index].set_style(style(byte_offset));
                // Reset following cells if multi-width (they would be hidden by the grapheme),
                for i in index + 1..index + width {
                    self.content[i].reset();
                }
                index += width;
                x_offset += width;
            }
            if ellipsis && x_offset - (x as usize) < string.width() {
                self.content[index].set_symbol("…");
            }
        } else {
            let mut start_index = self.index_of(x, y);
            let mut index = self.index_of(max_offset as u16, y);

            let content_width = string.width();
            let truncated = content_width > width;
            if ellipsis && truncated {
                self.content[start_index].set_symbol("…");
                start_index += 1;
            }
            if !truncated {
                index -= width - content_width;
            }
            for (byte_offset, s) in graphemes.rev() {
                let width = s.width();
                if width == 0 {
                    continue;
                }
                let start = index - width;
                if start < start_index {
                    break;
                }
                self.content[start].set_symbol(s);
                self.content[start].set_style(style(byte_offset));
                for i in start + 1..index {
                    self.content[i].reset();
                }
                index -= width;
                x_offset += width;
            }
        }
        (x_offset as u16, y)
    }

    /// Print at most the first `width` characters of a [Spans]  if enough space is available
    /// until the end of the line. Appends a `…` at the end of truncated lines.
    pub fn set_spans_truncated(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) {
        // prevent panic if out of range
        if !self.in_bounds(x, y) || width == 0 {
            return (x, y);
        }

        let mut x_offset = x as usize;
        let max_offset = min(self.area.right(), width.saturating_add(x));
        let mut start_index = self.index_of(x, y);
        let mut index = self.index_of(max_offset, y);

        let content_width = spans.width();
        let truncated = content_width > width as usize;
        if truncated {
            self.content[start_index].set_symbol("…");
            start_index += 1;
        } else {
            index -= width as usize - content_width;
        }
        for span in spans.0.iter().rev() {
            for s in span.content.graphemes(true).rev() {
                let width = s.width();
                if width == 0 {
                    continue;
                }
                let start = index - width;
                if start < start_index {
                    break;
                }
                self.content[start].set_symbol(s);
                self.content[start].set_style(span.style);
                for i in start + 1..index {
                    self.content[i].reset();
                }
                index -= width;
                x_offset += width;
            }
        }
        (x_offset as u16, y)
    }

    /// Print at most the first `width` characters of a [Spans] if enough space is available
    /// until the end of the line
    pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans, width: u16) -> (u16, u16) {
        let mut remaining_width = width;
        let mut x = x;
        for span in &spans.0 {
            if remaining_width == 0 {
                break;
            }
            let pos = self.set_stringn(
                x,
                y,
                span.content.as_ref(),
                remaining_width as usize,
                span.style,
            );
            let w = pos.0.saturating_sub(x);
            x = pos.0;
            remaining_width = remaining_width.saturating_sub(w);
        }
        (x, y)
    }

    /// Print at most the first `width` characters of a [Span] if enough space is available
    /// until the end of the line
    pub fn set_span(&mut self, x: u16, y: u16, span: &Span, width: u16) -> (u16, u16) {
        self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
    }

    #[deprecated(
        since = "0.10.0",
        note = "You should use styling capabilities of `Buffer::set_style`"
    )]
    pub fn set_background(&mut self, area: Rect, color: Color) {
        for y in area.top()..area.bottom() {
            for x in area.left()..area.right() {
                self[(x, y)].set_bg(color);
            }
        }
    }

    /// Set all cells in the [area](Rect) to the given [Style]
    pub fn set_style(&mut self, area: Rect, style: Style) {
        for y in area.top()..area.bottom() {
            for x in area.left()..area.right() {
                self[(x, y)].set_style(style);
            }
        }
    }

    /// Resize the buffer so that the mapped area matches the given area and that the buffer
    /// length is equal to area.width * area.height
    pub fn resize(&mut self, area: Rect) {
        let length = area.area();
        if self.content.len() > length {
            self.content.truncate(length);
        } else {
            self.content.resize(length, Default::default());
        }
        self.area = area;
    }

    /// Reset all cells in the buffer
    pub fn reset(&mut self) {
        for c in &mut self.content {
            c.reset();
        }
    }

    /// Clear an area in the buffer
    pub fn clear(&mut self, area: Rect) {
        for x in area.left()..area.right() {
            for y in area.top()..area.bottom() {
                self[(x, y)].reset();
            }
        }
    }

    /// Clear an area in the buffer with a default style.
    pub fn clear_with(&mut self, area: Rect, style: Style) {
        for x in area.left()..area.right() {
            for y in area.top()..area.bottom() {
                let cell = &mut self[(x, y)];
                cell.reset();
                cell.set_style(style);
            }
        }
    }

    /// Merge an other buffer into this one
    pub fn merge(&mut self, other: &Buffer) {
        let area = self.area.union(other.area);
        let cell: Cell = Default::default();
        self.content.resize(area.area(), cell.clone());

        // Move original content to the appropriate space
        let size = self.area.area();
        for i in (0..size).rev() {
            let (x, y) = self.pos_of(i);
            // New index in content
            let k = ((y - area.y) * area.width + x - area.x) as usize;
            if i != k {
                self.content[k] = self.content[i].clone();
                self.content[i] = cell.clone();
            }
        }

        // Push content of the other buffer into this one (may erase previous
        // data)
        let size = other.area.area();
        for i in 0..size {
            let (x, y) = other.pos_of(i);
            // New index in content
            let k = ((y - area.y) * area.width + x - area.x) as usize;
            self.content[k] = other.content[i].clone();
        }
        self.area = area;
    }

    /// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
    /// self to other.
    ///
    /// We're assuming that buffers are well-formed, that is no double-width cell is followed by
    /// a non-blank cell.
    ///
    /// # Multi-width characters handling:
    ///
    /// ```text
    /// (Index:) `01`
    /// Prev:    `コ`
    /// Next:    `aa`
    /// Updates: `0: a, 1: a'
    /// ```
    ///
    /// ```text
    /// (Index:) `01`
    /// Prev:    `a `
    /// Next:    `コ`
    /// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
    /// ```
    ///
    /// ```text
    /// (Index:) `012`
    /// Prev:    `aaa`
    /// Next:    `aコ`
    /// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
    /// ```
    pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
        let previous_buffer = &self.content;
        let next_buffer = &other.content;
        let width = self.area.width;

        let mut updates: Vec<(u16, u16, &Cell)> = vec![];
        // Cells invalidated by drawing/replacing preceding multi-width characters:
        let mut invalidated: usize = 0;
        // Cells from the current buffer to skip due to preceding multi-width characters taking their
        // place (the skipped cells should be blank anyway):
        let mut to_skip: usize = 0;
        for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
            if (current != previous || invalidated > 0) && to_skip == 0 {
                let x = (i % width as usize) as u16;
                let y = (i / width as usize) as u16;
                updates.push((x, y, &next_buffer[i]));
            }

            let current_width = current.width();
            to_skip = current_width.saturating_sub(1);

            let affected_width = std::cmp::max(current_width, previous.width());
            invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
        }
        updates
    }
}

impl std::ops::Index<(u16, u16)> for Buffer {
    type Output = Cell;

    fn index(&self, (x, y): (u16, u16)) -> &Self::Output {
        let i = self.index_of(x, y);
        &self.content[i]
    }
}

impl std::ops::IndexMut<(u16, u16)> for Buffer {
    fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output {
        let i = self.index_of(x, y);
        &mut self.content[i]
    }
}

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

    fn cell(s: &str) -> Cell {
        let mut cell = Cell::default();
        cell.set_symbol(s);
        cell
    }

    #[test]
    fn it_translates_to_and_from_coordinates() {
        let rect = Rect::new(200, 100, 50, 80);
        let buf = Buffer::empty(rect);

        // First cell is at the upper left corner.
        assert_eq!(buf.pos_of(0), (200, 100));
        assert_eq!(buf.index_of(200, 100), 0);

        // Last cell is in the lower right.
        assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
        assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
    }

    #[test]
    #[should_panic(expected = "outside the buffer")]
    #[cfg(debug_assertions)]
    fn pos_of_panics_on_out_of_bounds() {
        let rect = Rect::new(0, 0, 10, 10);
        let buf = Buffer::empty(rect);

        // There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
        buf.pos_of(100);
    }

    #[test]
    #[should_panic(expected = "outside the buffer")]
    #[cfg(debug_assertions)]
    fn index_of_panics_on_out_of_bounds() {
        let rect = Rect::new(0, 0, 10, 10);
        let buf = Buffer::empty(rect);

        // width is 10; zero-indexed means that 10 would be the 11th cell.
        buf.index_of(10, 0);
    }

    #[test]
    fn buffer_set_string() {
        let area = Rect::new(0, 0, 5, 1);
        let mut buffer = Buffer::empty(area);

        // Zero-width
        buffer.set_stringn(0, 0, "aaa", 0, Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["     "]));

        buffer.set_string(0, 0, "aaa", Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["aaa  "]));

        // Width limit:
        buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));

        buffer.set_string(0, 0, "12345", Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));

        // Width truncation:
        buffer.set_string(0, 0, "123456", Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
    }

    #[test]
    fn buffer_set_string_zero_width() {
        let area = Rect::new(0, 0, 1, 1);
        let mut buffer = Buffer::empty(area);

        // U+200B is the zero-width space codepoint
        assert_eq!("\u{200B}".width(), 0);

        // Leading grapheme with zero width
        let s = "\u{200B}a";
        buffer.set_stringn(0, 0, s, 1, Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["a"]));

        // Trailing grapheme with zero width
        let s = "a\u{200B}";
        buffer.set_stringn(0, 0, s, 1, Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
    }

    #[test]
    fn buffer_set_string_double_width() {
        let area = Rect::new(0, 0, 5, 1);
        let mut buffer = Buffer::empty(area);
        buffer.set_string(0, 0, "コン", Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));

        // Only 1 space left.
        buffer.set_string(0, 0, "コンピ", Style::default());
        assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
    }

    #[test]
    fn buffer_with_lines() {
        let buffer =
            Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
        assert_eq!(buffer.area.x, 0);
        assert_eq!(buffer.area.y, 0);
        assert_eq!(buffer.area.width, 10);
        assert_eq!(buffer.area.height, 4);
    }

    #[test]
    fn buffer_diffing_empty_empty() {
        let area = Rect::new(0, 0, 40, 40);
        let prev = Buffer::empty(area);
        let next = Buffer::empty(area);
        let diff = prev.diff(&next);
        assert_eq!(diff, vec![]);
    }

    #[test]
    fn buffer_diffing_empty_filled() {
        let area = Rect::new(0, 0, 40, 40);
        let prev = Buffer::empty(area);
        let next = Buffer::filled(area, Cell::default().set_symbol("a"));
        let diff = prev.diff(&next);
        assert_eq!(diff.len(), 40 * 40);
    }

    #[test]
    fn buffer_diffing_filled_filled() {
        let area = Rect::new(0, 0, 40, 40);
        let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
        let next = Buffer::filled(area, Cell::default().set_symbol("a"));
        let diff = prev.diff(&next);
        assert_eq!(diff, vec![]);
    }

    #[test]
    fn buffer_diffing_single_width() {
        let prev = Buffer::with_lines(vec![
            "          ",
            "┌Title─┐  ",
            "│      │  ",
            "│      │  ",
            "└──────┘  ",
        ]);
        let next = Buffer::with_lines(vec![
            "          ",
            "┌TITLE─┐  ",
            "│      │  ",
            "│      │  ",
            "└──────┘  ",
        ]);
        let diff = prev.diff(&next);
        assert_eq!(
            diff,
            vec![
                (2, 1, &cell("I")),
                (3, 1, &cell("T")),
                (4, 1, &cell("L")),
                (5, 1, &cell("E")),
            ]
        );
    }

    #[test]
    #[rustfmt::skip]
    fn buffer_diffing_multi_width() {
        let prev = Buffer::with_lines(vec![
            "┌Title─┐  ",
            "└──────┘  ",
        ]);
        let next = Buffer::with_lines(vec![
            "┌称号──┐  ",
            "└──────┘  ",
        ]);
        let diff = prev.diff(&next);
        assert_eq!(
            diff,
            vec![
                (1, 0, &cell("称")),
                // Skipped "i"
                (3, 0, &cell("号")),
                // Skipped "l"
                (5, 0, &cell("─")),
            ]
        );
    }

    #[test]
    fn buffer_diffing_multi_width_offset() {
        let prev = Buffer::with_lines(vec!["┌称号──┐"]);
        let next = Buffer::with_lines(vec!["┌─称号─┐"]);

        let diff = prev.diff(&next);
        assert_eq!(
            diff,
            vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
        );
    }

    #[test]
    fn buffer_merge() {
        let mut one = Buffer::filled(
            Rect {
                x: 0,
                y: 0,
                width: 2,
                height: 2,
            },
            Cell::default().set_symbol("1"),
        );
        let two = Buffer::filled(
            Rect {
                x: 0,
                y: 2,
                width: 2,
                height: 2,
            },
            Cell::default().set_symbol("2"),
        );
        one.merge(&two);
        assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
    }

    #[test]
    fn buffer_merge2() {
        let mut one = Buffer::filled(
            Rect {
                x: 2,
                y: 2,
                width: 2,
                height: 2,
            },
            Cell::default().set_symbol("1"),
        );
        let two = Buffer::filled(
            Rect {
                x: 0,
                y: 0,
                width: 2,
                height: 2,
            },
            Cell::default().set_symbol("2"),
        );
        one.merge(&two);
        assert_eq!(
            one,
            Buffer::with_lines(vec!["22  ", "22  ", "  11", "  11"])
        );
    }

    #[test]
    fn buffer_merge3() {
        let mut one = Buffer::filled(
            Rect {
                x: 3,
                y: 3,
                width: 2,
                height: 2,
            },
            Cell::default().set_symbol("1"),
        );
        let two = Buffer::filled(
            Rect {
                x: 1,
                y: 1,
                width: 3,
                height: 4,
            },
            Cell::default().set_symbol("2"),
        );
        one.merge(&two);
        let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
        merged.area = Rect {
            x: 1,
            y: 1,
            width: 4,
            height: 4,
        };
        assert_eq!(one, merged);
    }

    // ==================== Unicode / Emoji Tests ====================

    #[test]
    fn cell_set_symbol_various_unicode() {
        let test_cases = ["🔥", "👩‍💻", "🇫🇷", "👋🏽", "e\u{0301}", "한", "👨‍👩‍👧‍👦"];

        for symbol in test_cases {
            let mut cell = Cell::default();
            cell.set_symbol(symbol);
            assert_eq!(cell.symbol, *symbol);
        }
    }

    #[test]
    fn buffer_set_string_double_width_and_emoji() {
        let area = Rect::new(0, 0, 12, 1);
        let mut buffer = Buffer::empty(area);

        // Mix of emojis, CJK, and ASCII
        buffer.set_string(0, 0, "🔥漢a", Style::default());

        assert_eq!(&*buffer[(0, 0)].symbol, "🔥");
        assert_eq!(&*buffer[(1, 0)].symbol, " "); // Covered by emoji
        assert_eq!(&*buffer[(2, 0)].symbol, "漢");
        assert_eq!(&*buffer[(3, 0)].symbol, " "); // Covered by CJK
        assert_eq!(&*buffer[(4, 0)].symbol, "a");
    }

    #[test]
    fn buffer_set_string_combining_diacritics() {
        // "Eĥoŝanĝo ĉiuĵaŭde" using combining marks
        let text = "Eh\u{0302}os\u{0302}ang\u{0302}o c\u{0302}iuj\u{0302}au\u{0306}de";
        let area = Rect::new(0, 0, 20, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, text, Style::default());

        let graphemes: Vec<_> = text.graphemes(true).collect();
        for (i, &g) in graphemes.iter().enumerate() {
            assert_eq!(&*buffer[(i as u16, 0)].symbol, g);
        }
    }

    #[test]
    fn buffer_diffing_with_emoji() {
        let prev = Buffer::with_lines(vec!["🔥test"]);
        let next = Buffer::with_lines(vec!["xxtest"]);

        let diff = prev.diff(&next);

        // Should update positions where the emoji was
        assert!(diff.iter().any(|(x, _, c)| *x == 0 && &*c.symbol == "x"));
    }

    #[test]
    fn buffer_truncation_with_wide_chars() {
        let area = Rect::new(0, 0, 5, 1);
        let mut buffer = Buffer::empty(area);

        // "AAAA" (4) + "漢" (2) = 6, but buffer is 5 wide
        buffer.set_string(0, 0, "AAAA漢", Style::default());

        // Should not partially render the wide char
        assert_eq!(&*buffer[(4, 0)].symbol, " ");
    }

    #[test]
    fn buffer_overwrite_emoji_and_ascii() {
        let area = Rect::new(0, 0, 8, 1);
        let mut buffer = Buffer::empty(area);

        // Write emoji then overwrite with ASCII
        buffer.set_string(0, 0, "🔥🚀", Style::default());
        buffer.set_string(0, 0, "abcd", Style::default());

        assert_eq!(&*buffer[(0, 0)].symbol, "a");
        assert_eq!(&*buffer[(1, 0)].symbol, "b");

        // Write ASCII then overwrite with emoji
        buffer.set_string(0, 0, "abcd", Style::default());
        buffer.set_string(0, 0, "🔥🚀", Style::default());

        assert_eq!(&*buffer[(0, 0)].symbol, "🔥");
        assert_eq!(&*buffer[(2, 0)].symbol, "🚀");
    }

    // ==================== Adversarial Unicode Tests ====================
    // These test valid but unusual Unicode that stresses edge cases

    #[test]
    fn adversarial_zalgo_text() {
        // Base character with stacked combining marks
        let zalgo = "X\u{0303}\u{0304}\u{0305}";
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, zalgo, Style::default());

        assert_eq!(&*buffer[(0, 0)].symbol, zalgo);
        assert_eq!(&*buffer[(1, 0)].symbol, " ");
    }

    #[test]
    fn adversarial_zero_width_chars() {
        // Zero-width joiners and spaces
        let text = "a\u{200B}b\u{200D}c";
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, text, Style::default());

        // Zero-width chars shouldn't take space
        assert_eq!(&*buffer[(0, 0)].symbol, "a");
        // ZWJ attaches to the preceding character
        assert_eq!(&*buffer[(1, 0)].symbol, "b\u{200D}");
        assert_eq!(&*buffer[(2, 0)].symbol, "c");
    }

    #[test]
    fn adversarial_bidi_and_formatting() {
        // RTL override and BOM
        let bidi = "\u{202E}abc\u{202C}";
        let bom = "\u{FEFF}text";
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, bidi, Style::default());
        assert_eq!(&*buffer[(0, 0)].symbol, "a");

        buffer.set_string(0, 0, bom, Style::default());
        assert_eq!(&*buffer[(0, 0)].symbol, "t");
    }

    #[test]
    fn adversarial_supplementary_planes() {
        // High codepoints from supplementary planes
        let supp = "\u{10000}𝄞𒀀𓀀";
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, supp, Style::default());

        assert!(!buffer[(0, 0)].symbol.is_empty());
    }

    #[test]
    fn adversarial_regional_indicators() {
        // Unpaired and triple regional indicators
        let single_ri = "\u{1F1E6}";
        let three = "\u{1F1FA}\u{1F1F8}\u{1F1E6}"; // US flag + orphan
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, single_ri, Style::default());
        assert_eq!(&*buffer[(0, 0)].symbol, single_ri);

        buffer.set_string(0, 0, three, Style::default());
        assert!(!buffer[(0, 0)].symbol.is_empty());
    }

    #[test]
    fn adversarial_tag_sequence_england() {
        // Subdivision flag using tag sequence
        let england = "🏴\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}";
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, england, Style::default());
        assert_eq!(&*buffer[(0, 0)].symbol, england);
    }

    #[test]
    fn adversarial_private_use_and_specials() {
        // PUA and replacement characters
        let pua = "\u{E000}\u{FFFD}\u{FFFC}";
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, pua, Style::default());
        assert_eq!(&*buffer[(0, 0)].symbol, "\u{E000}");
    }

    #[test]
    fn adversarial_empty_string() {
        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);

        buffer.set_string(0, 0, "", Style::default());
        assert_eq!(&*buffer[(0, 0)].symbol, " ");
    }

    #[test]
    fn adversarial_extreme_zalgo_uses_replacement() {
        // Extreme zalgo: many combining characters exceed capacity
        let mut extreme = String::from("X");
        for _ in 0..50 {
            extreme.push('\u{0303}');
        }

        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);
        buffer.set_string(0, 0, &extreme, Style::default());

        // Should use replacement character
        assert_eq!(&*buffer[(0, 0)].symbol, REPLACEMENT_CHARACTER.to_string());
    }

    #[test]
    fn adversarial_extremely_long_grapheme_uses_replacement() {
        let mut absurd = String::from("o");
        for i in 0..100 {
            let combining = match i % 4 {
                0 => '\u{0300}',
                1 => '\u{0301}',
                2 => '\u{0302}',
                _ => '\u{0303}',
            };
            absurd.push(combining);
        }

        let area = Rect::new(0, 0, 10, 1);
        let mut buffer = Buffer::empty(area);
        buffer.set_string(0, 0, &absurd, Style::default());

        // Should use replacement character
        assert_eq!(&*buffer[(0, 0)].symbol, REPLACEMENT_CHARACTER.to_string());
    }

    #[test]
    fn adversarial_long_zwj_chain_uses_replacement() {
        let mut chain = String::from("👨");
        for _ in 0..10 {
            chain.push_str("\u{200D}👨");
        }

        let area = Rect::new(0, 0, 50, 1);
        let mut buffer = Buffer::empty(area);
        buffer.set_string(0, 0, &chain, Style::default());

        // Should use replacement character
        assert_eq!(&*buffer[(0, 0)].symbol, REPLACEMENT_CHARACTER.to_string());
    }
}
