//! Rendering and interaction for proportional scrollbars.
//!
//! This module provides the widget, glyph selection, and interaction helpers. The pure math lives
//! in [`crate::metrics`].
//!
//! # How the parts interact
//!
//! 1. Your app owns `content_len`, `viewport_len`, and `offset`.
//! 2. [`ScrollMetrics`] converts them into thumb geometry.
//! 3. [`ScrollBar`] renders using the selected [`GlyphSet`].
//! 4. Input events update `offset` via [`ScrollCommand`].
//!
//! The scrollbar renders only a single row or column. If you provide a larger [`Rect`], it will
//! still render into the first row/column of that area.
//!
//! Arrow endcaps are optional. When enabled, they consume one cell at the start/end of the track,
//! and the thumb renders inside the remaining inner area.
//!
//! Partial glyph selection uses [`CellFill::Partial`]: `start == 0` means the partial fill begins
//! at the leading edge (top/left), so the upper/left glyphs are chosen. Non-zero `start` uses the
//! lower/right glyphs to indicate a trailing-edge fill.
//!
//! Drag operations store a "grab offset" in subcells (1/8 of a cell; see [`crate::SUBCELL`]) so the
//! thumb does not jump when the pointer starts dragging; subsequent drag events subtract that
//! offset to keep the grab point stable.
//!
//! Wheel events are ignored unless their axis matches the scrollbar orientation. Positive deltas
//! scroll down/right.
//!
//! The example below renders a vertical scrollbar into a buffer. It demonstrates how the widget
//! uses `content_len`, `viewport_len`, and `offset` to decide the thumb size and position.
//!
//! ```rust
//! use ratatui_core::buffer::Buffer;
//! use ratatui_core::layout::Rect;
//! use ratatui_core::widgets::Widget;
//! use tui_scrollbar::{ScrollBar, ScrollLengths};
//!
//! let area = Rect::new(0, 0, 1, 4);
//! let lengths = ScrollLengths {
//!     content_len: 120,
//!     viewport_len: 40,
//! };
//! let scrollbar = ScrollBar::vertical(lengths).offset(20);
//!
//! let mut buffer = Buffer::empty(area);
//! scrollbar.render(area, &mut buffer);
//! ```
//!
//! [`Rect`]: ratatui_core::layout::Rect

#[cfg(feature = "crossterm")]
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use ratatui_core::widgets::Widget;

use crate::metrics::{CellFill, HitTest, ScrollMetrics, SUBCELL};

/// Axis the scrollbar is laid out on.
///
/// Orientation determines whether the track length is derived from height or width.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollBarOrientation {
    /// A vertical scrollbar that fills a single column.
    Vertical,
    /// A horizontal scrollbar that fills a single row.
    Horizontal,
}

/// Behavior when the user clicks on the track outside the thumb.
///
/// Page clicks move by `viewport_len`. Jump-to-click centers the thumb near the click.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackClickBehavior {
    /// Move by one viewport length toward the click position.
    Page,
    /// Jump the thumb toward the click position.
    JumpToClick,
}

/// Which arrow endcaps to render on the track.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollBarArrows {
    /// Do not render arrow endcaps.
    None,
    /// Render the arrow at the start of the track (top/left).
    Start,
    /// Render the arrow at the end of the track (bottom/right).
    End,
    /// Render arrows at both ends of the track.
    Both,
}

impl ScrollBarArrows {
    const fn has_start(self) -> bool {
        matches!(self, Self::Start | Self::Both)
    }

    const fn has_end(self) -> bool {
        matches!(self, Self::End | Self::Both)
    }
}

impl Default for ScrollBarArrows {
    fn default() -> Self {
        Self::Both
    }
}

/// Action requested by a pointer or wheel event.
///
/// Apply these to your stored offsets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollCommand {
    /// Update the content offset in the same logical units you supplied.
    SetOffset(usize),
}

/// Axis for scroll wheel events.
///
/// The scrollbar ignores wheel events that do not match its orientation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollAxis {
    /// Wheel scroll in the vertical direction.
    Vertical,
    /// Wheel scroll in the horizontal direction.
    Horizontal,
}

/// Pointer button used for interaction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointerButton {
    /// Primary pointer button (usually left mouse button).
    Primary,
}

/// Kind of pointer interaction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointerEventKind {
    /// Pointer pressed down.
    Down,
    /// Pointer moved while pressed down.
    Drag,
    /// Pointer released.
    Up,
}

/// Pointer input in terminal cell coordinates.
///
/// Use this to describe a pointer action relative to the scrollbar area.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PointerEvent {
    /// Column of the event, in terminal cells.
    pub column: u16,
    /// Row of the event, in terminal cells.
    pub row: u16,
    /// Event kind.
    pub kind: PointerEventKind,
    /// Pointer button.
    pub button: PointerButton,
}

/// Scroll wheel input with an axis and a signed delta.
///
/// Positive deltas scroll down/right, negative deltas scroll up/left. The `column` and `row` are
/// used to ignore wheel events outside the scrollbar area.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrollWheel {
    /// Axis the wheel is scrolling.
    pub axis: ScrollAxis,
    /// Signed delta. Positive values scroll down/right.
    pub delta: isize,
    /// Column where the wheel event occurred.
    pub column: u16,
    /// Row where the wheel event occurred.
    pub row: u16,
}

/// Backend-agnostic input event for a scrollbar.
///
/// Use this in input handling when you want to stay backend-agnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollEvent {
    /// Pointer down/drag/up events.
    Pointer(PointerEvent),
    /// Scroll wheel input.
    ScrollWheel(ScrollWheel),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ArrowHit {
    Start,
    End,
}

#[derive(Debug, Clone, Copy)]
struct ArrowLayout {
    track_area: Rect,
    start: Option<(u16, u16)>,
    end: Option<(u16, u16)>,
}

/// Drag state that should persist between frames.
///
/// Store this in your app state so drags remain active across draw calls.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrollBarInteraction {
    drag_state: DragState,
}

/// Internal drag capture state stored by [`ScrollBarInteraction`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DragState {
    /// No active drag.
    Idle,
    /// A drag is active; `grab_offset` is in subcells.
    Dragging { grab_offset: usize },
}

impl Default for DragState {
    fn default() -> Self {
        Self::Idle
    }
}

impl Default for ScrollBarInteraction {
    fn default() -> Self {
        Self {
            drag_state: DragState::default(),
        }
    }
}

impl ScrollBarInteraction {
    /// Creates a fresh interaction state with no active drag.
    pub fn new() -> Self {
        Self::default()
    }

    fn start_drag(&mut self, grab_offset: usize) {
        self.drag_state = DragState::Dragging { grab_offset };
    }

    fn stop_drag(&mut self) {
        self.drag_state = DragState::Idle;
    }
}

/// Glyphs used to render the track, arrows, and thumb.
///
/// Arrays use indices 0..=7 to represent 1/8th through full coverage.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlyphSet {
    /// Track glyph for vertical scrollbars.
    pub track_vertical: char,
    /// Track glyph for horizontal scrollbars.
    pub track_horizontal: char,
    /// Arrow glyph for the start of a vertical scrollbar (top).
    pub arrow_vertical_start: char,
    /// Arrow glyph for the end of a vertical scrollbar (bottom).
    pub arrow_vertical_end: char,
    /// Arrow glyph for the start of a horizontal scrollbar (left).
    pub arrow_horizontal_start: char,
    /// Arrow glyph for the end of a horizontal scrollbar (right).
    pub arrow_horizontal_end: char,
    /// Thumb glyphs for vertical lower fills (1/8th through full).
    pub thumb_vertical_lower: [char; 8],
    /// Thumb glyphs for vertical upper fills (1/8th through full).
    pub thumb_vertical_upper: [char; 8],
    /// Thumb glyphs for horizontal left fills (1/8th through full).
    pub thumb_horizontal_left: [char; 8],
    /// Thumb glyphs for horizontal right fills (1/8th through full).
    pub thumb_horizontal_right: [char; 8],
}

impl GlyphSet {
    /// Glyphs that mix standard block elements with legacy supplement glyphs.
    ///
    /// Use this to get full 1/8th coverage for upper and right edges that the standard block set
    /// lacks; these glyphs come from [Symbols for Legacy Computing].
    pub const fn legacy() -> Self {
        let vertical_lower = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
        let vertical_upper = ['▔', '🮂', '🮃', '▀', '🮄', '🮅', '🮆', '█'];
        let horizontal_left = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
        let horizontal_right = ['▕', '🮇', '🮈', '▐', '🮉', '🮊', '🮋', '█'];
        Self {
            track_vertical: '│',
            track_horizontal: '─',
            arrow_vertical_start: '▲',
            arrow_vertical_end: '▼',
            arrow_horizontal_start: '◄',
            arrow_horizontal_end: '►',
            thumb_vertical_lower: vertical_lower,
            thumb_vertical_upper: vertical_upper,
            thumb_horizontal_left: horizontal_left,
            thumb_horizontal_right: horizontal_right,
        }
    }

    /// Glyphs using only standard Unicode block elements.
    ///
    /// Use this if your font lacks the legacy glyphs; upper/right partials will use the same
    /// glyphs as lower/left partials.
    pub const fn unicode() -> Self {
        let vertical = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
        let horizontal = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
        Self {
            track_vertical: '│',
            track_horizontal: '─',
            arrow_vertical_start: '▲',
            arrow_vertical_end: '▼',
            arrow_horizontal_start: '◄',
            arrow_horizontal_end: '►',
            thumb_vertical_lower: vertical,
            thumb_vertical_upper: vertical,
            thumb_horizontal_left: horizontal,
            thumb_horizontal_right: horizontal,
        }
    }
}

impl Default for GlyphSet {
    fn default() -> Self {
        Self::legacy()
    }
}

/// [Symbols for Legacy Computing]: https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing

/// A proportional scrollbar widget with fractional thumb rendering.
///
/// # Key methods
///
/// - [`Self::new`]
/// - [`Self::orientation`]
/// - [`Self::arrows`]
/// - [`Self::content_len`]
/// - [`Self::viewport_len`]
/// - [`Self::offset`]
///
/// # Important
///
/// - `content_len` and `viewport_len` are in logical units.
/// - Zero values are treated as 1.
/// - The scrollbar renders into a single row or column.
///
/// # Behavior
///
/// The thumb length is proportional to `viewport_len / content_len` and clamped to at least one
/// full cell for usability. When `content_len <= viewport_len`, the thumb fills the track. Areas
/// with zero width or height render nothing.
///
/// Arrow endcaps, when enabled, consume one cell at the start/end of the track. The thumb and
/// track render in the remaining inner area. Clicking an arrow steps the offset by `scroll_step`.
///
/// # Styling
///
/// Track glyphs use `track_style`. Thumb glyphs use `thumb_style`.
///
/// # State
///
/// This widget is stateless. Pointer drag state lives in [`ScrollBarInteraction`].
///
/// # Examples
///
/// ```rust
/// use ratatui_core::buffer::Buffer;
/// use ratatui_core::layout::Rect;
/// use ratatui_core::widgets::Widget;
/// use tui_scrollbar::{ScrollBar, ScrollLengths};
///
/// let area = Rect::new(0, 0, 1, 5);
/// let lengths = ScrollLengths {
///     content_len: 200,
///     viewport_len: 40,
/// };
/// let scrollbar = ScrollBar::vertical(lengths).offset(60);
///
/// let mut buffer = Buffer::empty(area);
/// scrollbar.render(area, &mut buffer);
/// ```
///
/// ## Updating offsets on input
///
/// This is the typical pattern for pointer handling: feed events to the scrollbar and apply the
/// returned command to your stored offset.
///
/// ```rust,no_run
/// use ratatui_core::layout::Rect;
/// use tui_scrollbar::{
///     PointerButton, PointerEvent, PointerEventKind, ScrollBar, ScrollBarInteraction,
///     ScrollCommand, ScrollEvent, ScrollLengths,
/// };
///
/// let area = Rect::new(0, 0, 1, 10);
/// let lengths = ScrollLengths {
///     content_len: 400,
///     viewport_len: 80,
/// };
/// let scrollbar = ScrollBar::vertical(lengths).offset(0);
/// let mut interaction = ScrollBarInteraction::new();
/// let mut offset = 0;
///
/// let event = ScrollEvent::Pointer(PointerEvent {
///     column: 0,
///     row: 3,
///     kind: PointerEventKind::Down,
///     button: PointerButton::Primary,
/// });
///
/// if let Some(ScrollCommand::SetOffset(next)) =
///     scrollbar.handle_event(area, event, &mut interaction)
/// {
///     offset = next;
/// }
/// # let _ = offset;
/// ```
///
/// ## Track click behavior
///
/// Choose between classic page jumps or jump-to-click behavior.
///
/// ```rust
/// use tui_scrollbar::{ScrollBar, ScrollLengths, TrackClickBehavior};
///
/// let lengths = ScrollLengths {
///     content_len: 10,
///     viewport_len: 5,
/// };
/// let scrollbar =
///     ScrollBar::vertical(lengths).track_click_behavior(TrackClickBehavior::JumpToClick);
/// ```
///
/// ## Arrow endcaps
///
/// Arrow endcaps are optional. When enabled, they reserve one cell at each end of the track.
///
/// ```rust
/// use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths};
///
/// let lengths = ScrollLengths {
///     content_len: 120,
///     viewport_len: 24,
/// };
/// let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::Both);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScrollBar {
    orientation: ScrollBarOrientation,
    content_len: usize,
    viewport_len: usize,
    offset: usize,
    track_style: Style,
    thumb_style: Style,
    glyph_set: GlyphSet,
    arrows: ScrollBarArrows,
    track_click_behavior: TrackClickBehavior,
    scroll_step: usize,
}

impl ScrollBar {
    /// Creates a scrollbar with the given orientation and lengths.
    ///
    /// Zero lengths are treated as 1.
    ///
    /// ```rust
    /// use tui_scrollbar::{ScrollBar, ScrollBarOrientation, ScrollLengths};
    ///
    /// let lengths = ScrollLengths {
    ///     content_len: 120,
    ///     viewport_len: 40,
    /// };
    /// let scrollbar = ScrollBar::new(ScrollBarOrientation::Vertical, lengths);
    /// ```
    pub fn new(orientation: ScrollBarOrientation, lengths: crate::ScrollLengths) -> Self {
        Self {
            orientation,
            content_len: lengths.content_len,
            viewport_len: lengths.viewport_len,
            offset: 0,
            track_style: Style::default(),
            thumb_style: Style::default(),
            glyph_set: GlyphSet::default(),
            arrows: ScrollBarArrows::default(),
            track_click_behavior: TrackClickBehavior::Page,
            scroll_step: 1,
        }
    }

    /// Creates a vertical scrollbar with the given content and viewport lengths.
    pub fn vertical(lengths: crate::ScrollLengths) -> Self {
        Self::new(ScrollBarOrientation::Vertical, lengths)
    }

    /// Creates a horizontal scrollbar with the given content and viewport lengths.
    pub fn horizontal(lengths: crate::ScrollLengths) -> Self {
        Self::new(ScrollBarOrientation::Horizontal, lengths)
    }

    /// Sets the scrollbar orientation.
    pub const fn orientation(mut self, orientation: ScrollBarOrientation) -> Self {
        self.orientation = orientation;
        self
    }

    /// Sets the total scrollable content length in logical units.
    ///
    /// Larger values shrink the thumb, while smaller values enlarge it.
    ///
    /// Zero values are treated as 1.
    pub const fn content_len(mut self, content_len: usize) -> Self {
        self.content_len = content_len;
        self
    }

    /// Sets the visible viewport length in logical units.
    ///
    /// When `viewport_len >= content_len`, the thumb fills the track.
    ///
    /// Zero values are treated as 1.
    pub const fn viewport_len(mut self, viewport_len: usize) -> Self {
        self.viewport_len = viewport_len;
        self
    }

    /// Sets the current scroll offset in logical units.
    ///
    /// Offsets are clamped to `content_len - viewport_len` during rendering.
    pub const fn offset(mut self, offset: usize) -> Self {
        self.offset = offset;
        self
    }

    /// Sets the style applied to track glyphs.
    ///
    /// Track styling applies only where the thumb is not rendered.
    pub const fn track_style(mut self, style: Style) -> Self {
        self.track_style = style;
        self
    }

    /// Sets the style applied to thumb glyphs.
    ///
    /// Thumb styling overrides track styling for covered cells.
    pub const fn thumb_style(mut self, style: Style) -> Self {
        self.thumb_style = style;
        self
    }

    /// Selects the glyph set used to render the track and thumb.
    ///
    /// [`GlyphSet::legacy`] uses additional symbols for 1/8th upper/right fills. Use
    /// [`GlyphSet::unicode`] if you want to avoid the legacy supplement.
    pub const fn glyph_set(mut self, glyph_set: GlyphSet) -> Self {
        self.glyph_set = glyph_set;
        self
    }

    /// Sets which arrow endcaps are rendered.
    pub const fn arrows(mut self, arrows: ScrollBarArrows) -> Self {
        self.arrows = arrows;
        self
    }

    /// Sets behavior for clicks on the track outside the thumb.
    ///
    /// Use [`TrackClickBehavior::Page`] for classic page-up/down behavior, or
    /// [`TrackClickBehavior::JumpToClick`] to move the thumb toward the click.
    pub const fn track_click_behavior(mut self, behavior: TrackClickBehavior) -> Self {
        self.track_click_behavior = behavior;
        self
    }

    /// Sets the scroll step used for wheel events.
    ///
    /// The wheel delta is multiplied by this value (in your logical units) and then clamped.
    pub fn scroll_step(mut self, step: usize) -> Self {
        self.scroll_step = step.max(1);
        self
    }

    /// Handles a backend-agnostic scrollbar event.
    ///
    /// Returns a [`ScrollCommand`] when the event should update the offset.
    /// Events outside the scrollbar area are ignored.
    ///
    /// ```rust
    /// use ratatui_core::layout::Rect;
    /// use tui_scrollbar::{
    ///     PointerButton, PointerEvent, PointerEventKind, ScrollBar, ScrollBarInteraction,
    ///     ScrollEvent, ScrollLengths,
    /// };
    ///
    /// let area = Rect::new(0, 0, 1, 6);
    /// let lengths = ScrollLengths {
    ///     content_len: 120,
    ///     viewport_len: 24,
    /// };
    /// let scrollbar = ScrollBar::vertical(lengths).offset(0);
    /// let mut interaction = ScrollBarInteraction::new();
    /// let event = ScrollEvent::Pointer(PointerEvent {
    ///     column: 0,
    ///     row: 2,
    ///     kind: PointerEventKind::Down,
    ///     button: PointerButton::Primary,
    /// });
    ///
    /// let _ = scrollbar.handle_event(area, event, &mut interaction);
    /// ```
    pub fn handle_event(
        &self,
        area: Rect,
        event: ScrollEvent,
        interaction: &mut ScrollBarInteraction,
    ) -> Option<ScrollCommand> {
        if area.width == 0 || area.height == 0 {
            return None;
        }

        let layout = self.arrow_layout(area);
        let lengths = crate::ScrollLengths {
            content_len: self.content_len,
            viewport_len: self.viewport_len,
        };
        let track_cells = match self.orientation {
            ScrollBarOrientation::Vertical => layout.track_area.height,
            ScrollBarOrientation::Horizontal => layout.track_area.width,
        };
        let metrics = ScrollMetrics::new(lengths, self.offset, track_cells);

        match event {
            ScrollEvent::Pointer(event) => {
                if let Some(command) =
                    self.handle_arrow_pointer(&layout, metrics, event, interaction)
                {
                    return Some(command);
                }
                self.handle_pointer_event(layout.track_area, metrics, event, interaction)
            }
            ScrollEvent::ScrollWheel(event) => self.handle_scroll_wheel(area, metrics, event),
        }
    }

    #[cfg(feature = "crossterm")]
    /// Handles crossterm mouse events for this scrollbar.
    ///
    /// This helper converts crossterm events into [`ScrollEvent`] values before delegating to
    /// [`Self::handle_event`].
    pub fn handle_mouse_event(
        &self,
        area: Rect,
        event: MouseEvent,
        interaction: &mut ScrollBarInteraction,
    ) -> Option<ScrollCommand> {
        let event = match event.kind {
            MouseEventKind::Down(MouseButton::Left) => Some(ScrollEvent::Pointer(PointerEvent {
                column: event.column,
                row: event.row,
                kind: PointerEventKind::Down,
                button: PointerButton::Primary,
            })),
            MouseEventKind::Up(MouseButton::Left) => Some(ScrollEvent::Pointer(PointerEvent {
                column: event.column,
                row: event.row,
                kind: PointerEventKind::Up,
                button: PointerButton::Primary,
            })),
            MouseEventKind::Drag(MouseButton::Left) => Some(ScrollEvent::Pointer(PointerEvent {
                column: event.column,
                row: event.row,
                kind: PointerEventKind::Drag,
                button: PointerButton::Primary,
            })),
            MouseEventKind::ScrollUp => Some(ScrollEvent::ScrollWheel(ScrollWheel {
                axis: ScrollAxis::Vertical,
                delta: -1,
                column: event.column,
                row: event.row,
            })),
            MouseEventKind::ScrollDown => Some(ScrollEvent::ScrollWheel(ScrollWheel {
                axis: ScrollAxis::Vertical,
                delta: 1,
                column: event.column,
                row: event.row,
            })),
            MouseEventKind::ScrollLeft => Some(ScrollEvent::ScrollWheel(ScrollWheel {
                axis: ScrollAxis::Horizontal,
                delta: -1,
                column: event.column,
                row: event.row,
            })),
            MouseEventKind::ScrollRight => Some(ScrollEvent::ScrollWheel(ScrollWheel {
                axis: ScrollAxis::Horizontal,
                delta: 1,
                column: event.column,
                row: event.row,
            })),
            _ => None,
        };

        event.and_then(|event| self.handle_event(area, event, interaction))
    }

    fn glyph_for_vertical(&self, fill: CellFill) -> (char, Style) {
        match fill {
            CellFill::Empty => (self.glyph_set.track_vertical, self.track_style),
            CellFill::Full => (self.glyph_set.thumb_vertical_lower[7], self.thumb_style),
            CellFill::Partial { start, len } => {
                let index = len.saturating_sub(1) as usize;
                let glyph = if start == 0 {
                    self.glyph_set.thumb_vertical_upper[index]
                } else {
                    self.glyph_set.thumb_vertical_lower[index]
                };
                (glyph, self.thumb_style)
            }
        }
    }

    fn glyph_for_horizontal(&self, fill: CellFill) -> (char, Style) {
        match fill {
            CellFill::Empty => (self.glyph_set.track_horizontal, self.track_style),
            CellFill::Full => (self.glyph_set.thumb_horizontal_left[7], self.thumb_style),
            CellFill::Partial { start, len } => {
                let index = len.saturating_sub(1) as usize;
                let glyph = if start == 0 {
                    self.glyph_set.thumb_horizontal_left[index]
                } else {
                    self.glyph_set.thumb_horizontal_right[index]
                };
                (glyph, self.thumb_style)
            }
        }
    }

    fn handle_pointer_event(
        &self,
        area: Rect,
        metrics: ScrollMetrics,
        event: PointerEvent,
        interaction: &mut ScrollBarInteraction,
    ) -> Option<ScrollCommand> {
        if event.button != PointerButton::Primary {
            return None;
        }

        match event.kind {
            PointerEventKind::Down => {
                let cell_index = axis_cell_index(area, event.column, event.row, self.orientation)?;
                let position = cell_index
                    .saturating_mul(SUBCELL)
                    .saturating_add(SUBCELL / 2);
                if metrics.thumb_len() == 0 {
                    return None;
                }
                match metrics.hit_test(position) {
                    HitTest::Thumb => {
                        let grab_offset = position.saturating_sub(metrics.thumb_start());
                        interaction.start_drag(grab_offset);
                        None
                    }
                    HitTest::Track => {
                        interaction.stop_drag();
                        self.handle_track_click(metrics, position)
                    }
                }
            }
            PointerEventKind::Drag => match interaction.drag_state {
                DragState::Idle => None,
                DragState::Dragging { grab_offset } => {
                    let cell_index =
                        axis_cell_index_clamped(area, event.column, event.row, self.orientation)?;
                    let position = cell_index
                        .saturating_mul(SUBCELL)
                        .saturating_add(SUBCELL / 2);
                    let thumb_start = position.saturating_sub(grab_offset);
                    Some(ScrollCommand::SetOffset(
                        metrics.offset_for_thumb_start(thumb_start),
                    ))
                }
            },
            PointerEventKind::Up => {
                interaction.stop_drag();
                None
            }
        }
    }

    fn handle_scroll_wheel(
        &self,
        area: Rect,
        metrics: ScrollMetrics,
        event: ScrollWheel,
    ) -> Option<ScrollCommand> {
        if !area.contains((event.column, event.row).into()) {
            return None;
        }

        let matches_axis = match (self.orientation, event.axis) {
            (ScrollBarOrientation::Vertical, ScrollAxis::Vertical) => true,
            (ScrollBarOrientation::Horizontal, ScrollAxis::Horizontal) => true,
            _ => false,
        };

        if !matches_axis {
            return None;
        }

        let step = self.scroll_step.max(1) as isize;
        let delta = event.delta.saturating_mul(step);
        let max_offset = metrics.max_offset() as isize;
        let next = (metrics.offset() as isize).saturating_add(delta);
        let next = next.clamp(0, max_offset);
        Some(ScrollCommand::SetOffset(next as usize))
    }

    fn handle_track_click(&self, metrics: ScrollMetrics, position: usize) -> Option<ScrollCommand> {
        if metrics.max_offset() == 0 {
            return None;
        }

        match self.track_click_behavior {
            TrackClickBehavior::Page => {
                let thumb_end = metrics.thumb_start().saturating_add(metrics.thumb_len());
                if position < metrics.thumb_start() {
                    Some(ScrollCommand::SetOffset(
                        metrics.offset().saturating_sub(metrics.viewport_len()),
                    ))
                } else if position >= thumb_end {
                    Some(ScrollCommand::SetOffset(
                        (metrics.offset() + metrics.viewport_len()).min(metrics.max_offset()),
                    ))
                } else {
                    None
                }
            }
            TrackClickBehavior::JumpToClick => {
                let half_thumb = metrics.thumb_len() / 2;
                let thumb_start = position.saturating_sub(half_thumb);
                Some(ScrollCommand::SetOffset(
                    metrics.offset_for_thumb_start(thumb_start),
                ))
            }
        }
    }

    fn arrow_layout(&self, area: Rect) -> ArrowLayout {
        let mut track_area = area;
        let (start, end) = match self.orientation {
            ScrollBarOrientation::Vertical => {
                let start_enabled = self.arrows.has_start() && area.height > 0;
                let end_enabled = self.arrows.has_end() && area.height > start_enabled as u16;
                let start = start_enabled.then_some((area.x, area.y));
                let end = end_enabled
                    .then_some((area.x, area.y.saturating_add(area.height).saturating_sub(1)));
                if start_enabled {
                    track_area.y = track_area.y.saturating_add(1);
                    track_area.height = track_area.height.saturating_sub(1);
                }
                if end_enabled {
                    track_area.height = track_area.height.saturating_sub(1);
                }
                (start, end)
            }
            ScrollBarOrientation::Horizontal => {
                let start_enabled = self.arrows.has_start() && area.width > 0;
                let end_enabled = self.arrows.has_end() && area.width > start_enabled as u16;
                let start = start_enabled.then_some((area.x, area.y));
                let end = end_enabled
                    .then_some((area.x.saturating_add(area.width).saturating_sub(1), area.y));
                if start_enabled {
                    track_area.x = track_area.x.saturating_add(1);
                    track_area.width = track_area.width.saturating_sub(1);
                }
                if end_enabled {
                    track_area.width = track_area.width.saturating_sub(1);
                }
                (start, end)
            }
        };

        ArrowLayout {
            track_area,
            start,
            end,
        }
    }

    fn arrow_hit(&self, layout: &ArrowLayout, event: PointerEvent) -> Option<ArrowHit> {
        if let Some((x, y)) = layout.start {
            if event.column == x && event.row == y {
                return Some(ArrowHit::Start);
            }
        }
        if let Some((x, y)) = layout.end {
            if event.column == x && event.row == y {
                return Some(ArrowHit::End);
            }
        }
        None
    }

    fn handle_arrow_pointer(
        &self,
        layout: &ArrowLayout,
        metrics: ScrollMetrics,
        event: PointerEvent,
        interaction: &mut ScrollBarInteraction,
    ) -> Option<ScrollCommand> {
        if event.button != PointerButton::Primary || event.kind != PointerEventKind::Down {
            return None;
        }

        let hit = self.arrow_hit(layout, event)?;
        if metrics.max_offset() == 0 {
            return None;
        }

        interaction.stop_drag();
        let step = self.scroll_step.max(1) as isize;
        let delta = match hit {
            ArrowHit::Start => -step,
            ArrowHit::End => step,
        };
        let max_offset = metrics.max_offset() as isize;
        let next = (metrics.offset() as isize).saturating_add(delta);
        let next = next.clamp(0, max_offset);
        Some(ScrollCommand::SetOffset(next as usize))
    }

    fn render_arrows(&self, layout: &ArrowLayout, buf: &mut Buffer) {
        if let Some((x, y)) = layout.start {
            let glyph = match self.orientation {
                ScrollBarOrientation::Vertical => self.glyph_set.arrow_vertical_start,
                ScrollBarOrientation::Horizontal => self.glyph_set.arrow_horizontal_start,
            };
            let cell = &mut buf[(x, y)];
            cell.set_char(glyph);
            cell.set_style(self.track_style);
        }
        if let Some((x, y)) = layout.end {
            let glyph = match self.orientation {
                ScrollBarOrientation::Vertical => self.glyph_set.arrow_vertical_end,
                ScrollBarOrientation::Horizontal => self.glyph_set.arrow_horizontal_end,
            };
            let cell = &mut buf[(x, y)];
            cell.set_char(glyph);
            cell.set_style(self.track_style);
        }
    }
}

fn axis_cell_index(
    area: Rect,
    column: u16,
    row: u16,
    orientation: ScrollBarOrientation,
) -> Option<usize> {
    match orientation {
        ScrollBarOrientation::Vertical => {
            if row < area.y || row >= area.y.saturating_add(area.height) {
                None
            } else {
                Some(row.saturating_sub(area.y) as usize)
            }
        }
        ScrollBarOrientation::Horizontal => {
            if column < area.x || column >= area.x.saturating_add(area.width) {
                None
            } else {
                Some(column.saturating_sub(area.x) as usize)
            }
        }
    }
}

fn axis_cell_index_clamped(
    area: Rect,
    column: u16,
    row: u16,
    orientation: ScrollBarOrientation,
) -> Option<usize> {
    match orientation {
        ScrollBarOrientation::Vertical => {
            if area.height == 0 {
                return None;
            }
            let end = area.y.saturating_add(area.height).saturating_sub(1);
            let row = row.clamp(area.y, end);
            Some(row.saturating_sub(area.y) as usize)
        }
        ScrollBarOrientation::Horizontal => {
            if area.width == 0 {
                return None;
            }
            let end = area.x.saturating_add(area.width).saturating_sub(1);
            let column = column.clamp(area.x, end);
            Some(column.saturating_sub(area.x) as usize)
        }
    }
}

impl Widget for &ScrollBar {
    fn render(self, area: Rect, buf: &mut Buffer) {
        if area.width == 0 || area.height == 0 {
            return;
        }

        let layout = self.arrow_layout(area);
        self.render_arrows(&layout, buf);
        if layout.track_area.width == 0 || layout.track_area.height == 0 {
            return;
        }

        match self.orientation {
            ScrollBarOrientation::Vertical => {
                let metrics = ScrollMetrics::new(
                    crate::ScrollLengths {
                        content_len: self.content_len,
                        viewport_len: self.viewport_len,
                    },
                    self.offset,
                    layout.track_area.height,
                );
                let x = layout.track_area.x;
                for (idx, y) in (layout.track_area.y
                    ..layout.track_area.y.saturating_add(layout.track_area.height))
                    .enumerate()
                {
                    let (glyph, style) = self.glyph_for_vertical(metrics.cell_fill(idx));
                    let cell = &mut buf[(x, y)];
                    cell.set_char(glyph);
                    cell.set_style(style);
                }
            }
            ScrollBarOrientation::Horizontal => {
                let metrics = ScrollMetrics::new(
                    crate::ScrollLengths {
                        content_len: self.content_len,
                        viewport_len: self.viewport_len,
                    },
                    self.offset,
                    layout.track_area.width,
                );
                let y = layout.track_area.y;
                for (idx, x) in (layout.track_area.x
                    ..layout.track_area.x.saturating_add(layout.track_area.width))
                    .enumerate()
                {
                    let (glyph, style) = self.glyph_for_horizontal(metrics.cell_fill(idx));
                    let cell = &mut buf[(x, y)];
                    cell.set_char(glyph);
                    cell.set_style(style);
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use ratatui_core::buffer::Buffer;
    use ratatui_core::layout::Rect;

    use super::*;
    use crate::ScrollLengths;
    #[test]
    fn render_vertical_fractional_thumb() {
        let scrollbar = ScrollBar::vertical(ScrollLengths {
            content_len: 10,
            viewport_len: 3,
        })
        .arrows(ScrollBarArrows::None)
        .offset(1);
        let mut buf = Buffer::empty(Rect::new(0, 0, 1, 4));
        (&scrollbar).render(buf.area, &mut buf);
        assert_eq!(buf, Buffer::with_lines(vec!["▅", "▀", "│", "│"]));
    }

    #[test]
    fn render_horizontal_fractional_thumb() {
        let scrollbar = ScrollBar::horizontal(ScrollLengths {
            content_len: 10,
            viewport_len: 3,
        })
        .arrows(ScrollBarArrows::None)
        .offset(1);
        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
        (&scrollbar).render(buf.area, &mut buf);
        assert_eq!(buf, Buffer::with_lines(vec!["🮉▌──"]));
    }

    #[test]
    fn render_full_thumb_when_no_scroll() {
        let scrollbar = ScrollBar::vertical(ScrollLengths {
            content_len: 5,
            viewport_len: 10,
        })
        .arrows(ScrollBarArrows::None);
        let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
        (&scrollbar).render(buf.area, &mut buf);
        assert_eq!(buf, Buffer::with_lines(vec!["█", "█", "█"]));
    }

    #[test]
    fn render_vertical_arrows() {
        let scrollbar = ScrollBar::vertical(ScrollLengths {
            content_len: 5,
            viewport_len: 2,
        });
        let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
        (&scrollbar).render(buf.area, &mut buf);
        assert_eq!(buf, Buffer::with_lines(vec!["▲", "█", "▼"]));
    }

    #[test]
    fn render_horizontal_arrows() {
        let scrollbar = ScrollBar::horizontal(ScrollLengths {
            content_len: 5,
            viewport_len: 2,
        });
        let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
        (&scrollbar).render(buf.area, &mut buf);
        assert_eq!(buf, Buffer::with_lines(vec!["◄█►"]));
    }

    #[test]
    fn handle_track_click_pages() {
        let scrollbar = ScrollBar::vertical(ScrollLengths {
            content_len: 100,
            viewport_len: 20,
        })
        .arrows(ScrollBarArrows::None)
        .offset(40);
        let area = Rect::new(0, 0, 1, 10);
        let mut interaction = ScrollBarInteraction::default();
        let event = ScrollEvent::Pointer(PointerEvent {
            column: 0,
            row: 0,
            kind: PointerEventKind::Down,
            button: PointerButton::Primary,
        });
        let metrics = ScrollMetrics::new(
            ScrollLengths {
                content_len: 100,
                viewport_len: 20,
            },
            40,
            area.height,
        );
        let expected = metrics.offset().saturating_sub(metrics.viewport_len());
        assert_eq!(
            scrollbar.handle_event(area, event, &mut interaction),
            Some(ScrollCommand::SetOffset(expected))
        );
    }

    #[test]
    fn handle_drag_updates_offset() {
        let scrollbar = ScrollBar::vertical(ScrollLengths {
            content_len: 100,
            viewport_len: 20,
        })
        .arrows(ScrollBarArrows::None)
        .offset(40);
        let area = Rect::new(0, 0, 1, 10);
        let metrics = ScrollMetrics::new(
            ScrollLengths {
                content_len: 100,
                viewport_len: 20,
            },
            40,
            area.height,
        );
        let mut interaction = ScrollBarInteraction::default();
        let down = ScrollEvent::Pointer(PointerEvent {
            column: 0,
            row: 4,
            kind: PointerEventKind::Down,
            button: PointerButton::Primary,
        });
        assert_eq!(scrollbar.handle_event(area, down, &mut interaction), None);

        let drag = ScrollEvent::Pointer(PointerEvent {
            column: 0,
            row: 6,
            kind: PointerEventKind::Drag,
            button: PointerButton::Primary,
        });
        let grab_offset = (4 * SUBCELL + SUBCELL / 2).saturating_sub(metrics.thumb_start());
        let expected_thumb_start = (6 * SUBCELL + SUBCELL / 2).saturating_sub(grab_offset);
        let expected = metrics.offset_for_thumb_start(expected_thumb_start);
        assert_eq!(
            scrollbar.handle_event(area, drag, &mut interaction),
            Some(ScrollCommand::SetOffset(expected))
        );
    }

    #[test]
    fn handle_scroll_wheel_uses_step() {
        let scrollbar = ScrollBar::vertical(ScrollLengths {
            content_len: 100,
            viewport_len: 20,
        })
        .arrows(ScrollBarArrows::None)
        .offset(40)
        .scroll_step(3);
        let area = Rect::new(0, 0, 1, 10);
        let mut interaction = ScrollBarInteraction::default();
        let event = ScrollEvent::ScrollWheel(ScrollWheel {
            axis: ScrollAxis::Vertical,
            delta: 1,
            column: 0,
            row: 0,
        });
        assert_eq!(
            scrollbar.handle_event(area, event, &mut interaction),
            Some(ScrollCommand::SetOffset(43))
        );
    }

    #[test]
    fn handle_arrow_click_steps_offset() {
        let scrollbar = ScrollBar::vertical(ScrollLengths {
            content_len: 100,
            viewport_len: 20,
        })
        .offset(10)
        .scroll_step(5);
        let area = Rect::new(0, 0, 1, 5);
        let mut interaction = ScrollBarInteraction::default();
        let up = ScrollEvent::Pointer(PointerEvent {
            column: 0,
            row: 0,
            kind: PointerEventKind::Down,
            button: PointerButton::Primary,
        });
        assert_eq!(
            scrollbar.handle_event(area, up, &mut interaction),
            Some(ScrollCommand::SetOffset(5))
        );

        let down = ScrollEvent::Pointer(PointerEvent {
            column: 0,
            row: 4,
            kind: PointerEventKind::Down,
            button: PointerButton::Primary,
        });
        assert_eq!(
            scrollbar.handle_event(area, down, &mut interaction),
            Some(ScrollCommand::SetOffset(15))
        );
    }
}
