//! Smooth scrollbar widget with follow-mode awareness.
//!
//! This module provides scrollbar rendering using `tui-scrollbar` for smooth
//! Unicode-based scrollbars with sub-character precision.
//!
//! # Visual Design
//!
//! The scrollbar visibility indicates follow mode state:
//! - **Following (at bottom):** Very dim scrollbar (subtle indicator of content above)
//! - **Not following:** Brighter scrollbar (draws attention to "scrolled up" state)
//!
//! This helps users understand when they're viewing live content vs. scrolled back.
//!
//! # Layout
//!
//! Callers should reserve space for the scrollbar:
//! - 1 column gap (visual separation from content)
//! - 1 column track (the scrollbar itself)
//!
//! Use [`split_area_for_scrollbar`] to compute content and scrollbar areas.
//!
//! # TODO: Mouse Support
//!
//! `tui-scrollbar` already provides mouse interaction support via:
//! - [`tui_scrollbar::ScrollBarInteraction`] for drag state
//! - [`tui_scrollbar::ScrollEvent`] / [`tui_scrollbar::PointerEvent`] for input
//! - [`tui_scrollbar::ScrollBar::handle_event`] for hit testing and drag math
//!
//! To wire this up:
//! 1. Store `ScrollBarInteraction` in pane state
//! 2. Translate crossterm `MouseEvent` to `tui_scrollbar::PointerEvent`
//! 3. Call `scrollbar.handle_event()` to get `ScrollCommand::SetOffset`
//! 4. Update scroll position accordingly

use std::sync::atomic::{AtomicBool, Ordering};

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui_core::buffer::Buffer as CoreBuffer;
use ratatui_core::layout::Rect as CoreRect;
use ratatui_core::widgets::Widget as _;
use tui_scrollbar::ScrollBar;
use tui_scrollbar::ScrollLengths;
use tui_scrollbar::{SUBCELL, ScrollMetrics};

/// When set, every scrollbar renders as a no-op. The pager toggles this on in
/// minimal (scrollback-native) mode, where lists/dropdowns show
/// no scrollbar bar at all — they scroll internally and the footer carries the
/// "↑/↓ navigate" hint. Off (default) everywhere else, so the full TUI is
/// unaffected.
static SCROLLBARS_HIDDEN: AtomicBool = AtomicBool::new(false);

/// Globally hide or show all scrollbars. See [`SCROLLBARS_HIDDEN`].
pub fn set_scrollbars_hidden(hidden: bool) {
    SCROLLBARS_HIDDEN.store(hidden, Ordering::Relaxed);
}

/// Whether scrollbars are currently globally hidden.
pub fn scrollbars_hidden() -> bool {
    SCROLLBARS_HIDDEN.load(Ordering::Relaxed)
}

/// Number of columns reserved between content and the scrollbar track.
/// This creates the "X" gap in the XSXBXX pattern (gap between selection_right and scrollbar).
const SCROLLBAR_GAP_COLS: u16 = 1;

/// Width of the scrollbar track itself (in terminal cells).
const SCROLLBAR_TRACK_COLS: u16 = 1;

/// Total columns reserved for scrollbar UI (gap + track).
pub const SCROLLBAR_TOTAL_COLS: u16 = SCROLLBAR_GAP_COLS + SCROLLBAR_TRACK_COLS;

/// Split an area into content + scrollbar regions.
///
/// Layout:
/// - `content_area`: original area minus [`SCROLLBAR_TOTAL_COLS`] on the right
/// - `scrollbar_area`: the last column of the original area (1 cell wide)
/// - The column between them is the "gap" (left intentionally blank)
///
/// Returns `(content_area, None)` when the terminal is too narrow.
///
/// **Note**: This always reserves space for scrollbar. Use [`maybe_split_for_scrollbar`]
/// to only reserve space when the scrollbar will actually be shown.
pub fn split_area_for_scrollbar(area: Rect) -> (Rect, Option<Rect>) {
    if area.width <= SCROLLBAR_TOTAL_COLS {
        return (area, None);
    }

    let content_width = area.width.saturating_sub(SCROLLBAR_TOTAL_COLS);
    let content_area = Rect {
        x: area.x,
        y: area.y,
        width: content_width,
        height: area.height,
    };
    let scrollbar_area = Rect {
        x: area.right().saturating_sub(1),
        y: area.y,
        width: SCROLLBAR_TRACK_COLS,
        height: area.height,
    };

    (content_area, Some(scrollbar_area))
}

/// Split an area only if scrollbar is actually needed.
///
/// Unlike [`split_area_for_scrollbar`], this gives full width to content
/// when scrollbar won't be shown (`total_lines <= viewport_lines`).
///
/// Use this when you know the content height before splitting.
pub fn maybe_split_for_scrollbar(area: Rect, total_lines: u16) -> (Rect, Option<Rect>) {
    // Only reserve space if scrollbar will actually be shown
    if needs_scrollbar(total_lines, area.height) {
        split_area_for_scrollbar(area)
    } else {
        // No scrollbar needed - give full width to content
        (area, None)
    }
}

/// Whether the scrollbar should be shown (content overflows viewport).
pub fn needs_scrollbar(total_lines: u16, viewport_lines: u16) -> bool {
    total_lines > viewport_lines
}

/// Whether the view is at the bottom (following mode position).
#[allow(dead_code)] // Useful helper, kept for future use
pub fn is_at_bottom(total_lines: u16, viewport_lines: u16, offset: u16) -> bool {
    let max_offset = total_lines.saturating_sub(viewport_lines);
    offset >= max_offset
}

/// Result of mapping a scrollbar click/drag position to a scroll offset.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollbarClickResult {
    /// Jump to the very top (click on first row of track).
    Top,
    /// Jump to the very bottom (click on last row of track).
    Bottom,
    /// Set scroll offset to this value (proportional position).
    Offset(usize),
}

/// Map a click on the scrollbar gutter to a scroll offset.
///
/// Uses the same `tui_scrollbar::ScrollMetrics` that the renderer uses to
/// position the thumb, so the click is the exact inverse of the rendering.
/// Emulates `JumpToClick` behavior: centers the thumb on the click position.
///
/// # Arguments
///
/// * `cell_index` — 0-based row within the scrollbar area (screen_y - sb.y)
/// * `track_cells` — height of the scrollbar area (sb.height)
/// * `total_lines` — total content height (pre-scaled)
/// * `viewport_lines` — viewport height
///
/// Returns `Top`/`Bottom` for clicks on the first/last row, otherwise
/// an offset that places the thumb centered on the click.
pub fn scrollbar_click_to_offset(
    cell_index: u16,
    track_cells: u16,
    total_lines: u16,
    viewport_lines: u16,
) -> ScrollbarClickResult {
    if track_cells == 0 {
        return ScrollbarClickResult::Top;
    }

    // First row → go to top.
    if cell_index == 0 {
        return ScrollbarClickResult::Top;
    }
    // Last row → go to bottom.
    if cell_index >= track_cells.saturating_sub(1) {
        return ScrollbarClickResult::Bottom;
    }

    let lengths = ScrollLengths {
        content_len: total_lines as usize,
        viewport_len: viewport_lines as usize,
    };
    let metrics = ScrollMetrics::new(lengths, 0, track_cells);

    // Center the thumb on the clicked cell (same as tui_scrollbar JumpToClick).
    let position = (cell_index as usize)
        .saturating_mul(SUBCELL)
        .saturating_add(SUBCELL / 2);
    let half_thumb = metrics.thumb_len() / 2;
    let thumb_start = position.saturating_sub(half_thumb);
    let offset = metrics.offset_for_thumb_start(thumb_start);

    ScrollbarClickResult::Offset(offset)
}

/// Render a scrollbar with follow-mode aware styling.
///
/// # Arguments
///
/// * `buf` - The ratatui buffer to render into
/// * `scrollbar_area` - The 1-column area for the scrollbar track
/// * `total_lines` - Total content height in lines
/// * `viewport_lines` - Visible viewport height in lines
/// * `offset` - Current scroll offset (lines from top)
/// * `is_following` - Whether follow mode is active (dims the scrollbar)
///
/// The scrollbar is always rendered when content overflows, but styled differently
/// based on follow state:
/// - Following: very dim (subtle indicator)
/// - Not following: brighter (draws attention)
pub fn render_scrollbar(
    buf: &mut Buffer,
    scrollbar_area: Option<Rect>,
    total_lines: u16,
    viewport_lines: u16,
    offset: u16,
    is_following: bool,
) {
    if SCROLLBARS_HIDDEN.load(Ordering::Relaxed) {
        return;
    }

    let Some(scrollbar_area) = scrollbar_area else {
        return;
    };

    if scrollbar_area.width == 0 || scrollbar_area.height == 0 {
        return;
    }

    if !needs_scrollbar(total_lines, viewport_lines) {
        return;
    }

    let lengths = ScrollLengths {
        content_len: total_lines as usize,
        viewport_len: viewport_lines as usize,
    };

    let scrollbar = ScrollBar::vertical(lengths).offset(offset as usize);

    // Render into ratatui-core scratch buffer
    let core_area = CoreRect {
        x: scrollbar_area.x,
        y: scrollbar_area.y,
        width: scrollbar_area.width,
        height: scrollbar_area.height,
    };
    let mut scratch = CoreBuffer::empty(core_area);
    (&scrollbar).render(core_area, &mut scratch);

    // Copy to ratatui buffer with follow-aware styling
    let (track_style, thumb_style) = scrollbar_styles(is_following);
    for row in 0..scrollbar_area.height {
        let x = scrollbar_area.x;
        let y = scrollbar_area.y + row;
        let src = &scratch[(x, y)];
        let dst = &mut buf[(x, y)];
        if src.symbol() == " " {
            dst.set_symbol(" ");
            dst.set_style(track_style);
        } else {
            dst.set_symbol("\u{2588}");
            dst.set_style(thumb_style);
        }
    }
}

/// Get track and thumb styles based on follow mode.
///
/// Following mode: very dim colors (scrollbar recedes into background)
/// Not following: brighter colors (scrollbar "pops out")
fn scrollbar_styles(is_following: bool) -> (Style, Style) {
    let theme = crate::theme::Theme::current();
    if is_following {
        // Very dim - scrollbar is subtle when following
        let track_style = Style::new().bg(theme.scrollbar_bg);
        let thumb_style = Style::new().fg(theme.scrollbar_fg).bg(theme.scrollbar_bg);
        (track_style, thumb_style)
    } else {
        // Brighter - scrollbar stands out when scrolled up
        let track_style = Style::new().bg(theme.bg_highlight);
        let thumb_style = Style::new().fg(theme.gray).bg(theme.bg_highlight);
        (track_style, thumb_style)
    }
}

/// Render a scrollbar with custom track and thumb styles.
///
/// Like [`render_scrollbar`] but allows custom styling for theme integration.
pub fn render_scrollbar_styled(
    buf: &mut Buffer,
    scrollbar_area: Option<Rect>,
    total_lines: u16,
    viewport_lines: u16,
    offset: u16,
    track_style: Style,
    thumb_style: Style,
) {
    if SCROLLBARS_HIDDEN.load(Ordering::Relaxed) {
        return;
    }

    let Some(scrollbar_area) = scrollbar_area else {
        return;
    };

    if scrollbar_area.width == 0 || scrollbar_area.height == 0 {
        return;
    }

    if !needs_scrollbar(total_lines, viewport_lines) {
        return;
    }

    let lengths = ScrollLengths {
        content_len: total_lines as usize,
        viewport_len: viewport_lines as usize,
    };

    let scrollbar = ScrollBar::vertical(lengths).offset(offset as usize);

    // Render into ratatui-core scratch buffer
    let core_area = CoreRect {
        x: scrollbar_area.x,
        y: scrollbar_area.y,
        width: scrollbar_area.width,
        height: scrollbar_area.height,
    };
    let mut scratch = CoreBuffer::empty(core_area);
    (&scrollbar).render(core_area, &mut scratch);

    // Copy to ratatui buffer with custom styling
    for row in 0..scrollbar_area.height {
        let x = scrollbar_area.x;
        let y = scrollbar_area.y + row;
        let src = &scratch[(x, y)];
        let dst = &mut buf[(x, y)];
        if src.symbol() == " " {
            dst.set_symbol(" ");
            dst.set_style(track_style);
        } else {
            dst.set_symbol("\u{2588}");
            dst.set_style(thumb_style);
        }
    }
}

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

    #[test]
    fn test_split_area_normal() {
        let area = Rect::new(0, 0, 40, 10);
        let (content, scrollbar) = split_area_for_scrollbar(area);

        // Content should be 40 - 2 = 38 wide (gap + track)
        assert_eq!(content.width, 38);
        assert_eq!(content.height, 10);

        // Scrollbar should be at x=39, 1 column wide
        let sb = scrollbar.expect("scrollbar area");
        assert_eq!(sb.x, 39);
        assert_eq!(sb.width, 1);
        assert_eq!(sb.height, 10);
    }

    #[test]
    fn test_split_area_too_narrow() {
        let area = Rect::new(0, 0, 2, 10);
        let (content, scrollbar) = split_area_for_scrollbar(area);

        // Too narrow - return original area, no scrollbar
        assert_eq!(content, area);
        assert!(scrollbar.is_none());
    }

    #[test]
    fn test_maybe_split_reserves_when_needed() {
        let area = Rect::new(0, 0, 40, 10);

        // Content overflows (20 > 10) - should reserve scrollbar space
        let (content, scrollbar) = maybe_split_for_scrollbar(area, 20);
        assert_eq!(content.width, 38); // Reduced by 2 for gap + scrollbar track
        assert!(scrollbar.is_some());
    }

    #[test]
    fn test_maybe_split_full_width_when_not_needed() {
        let area = Rect::new(0, 0, 40, 10);

        // Content fits (5 <= 10) - should give full width to content
        let (content, scrollbar) = maybe_split_for_scrollbar(area, 5);
        assert_eq!(content.width, 40); // Full width
        assert!(scrollbar.is_none());
    }

    #[test]
    fn test_needs_scrollbar() {
        assert!(needs_scrollbar(100, 10)); // Content > viewport
        assert!(!needs_scrollbar(10, 10)); // Content == viewport
        assert!(!needs_scrollbar(5, 10)); // Content < viewport
    }

    #[test]
    fn test_is_at_bottom() {
        // total=100, viewport=10 -> max_offset=90
        assert!(is_at_bottom(100, 10, 90)); // At bottom
        assert!(is_at_bottom(100, 10, 95)); // Past bottom (clamped)
        assert!(!is_at_bottom(100, 10, 89)); // One line above bottom
        assert!(!is_at_bottom(100, 10, 0)); // At top
    }

    #[test]
    fn test_render_scrollbar_no_area() {
        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
        // Should not panic with None area
        render_scrollbar(&mut buf, None, 100, 10, 0, false);
    }

    #[test]
    fn test_render_scrollbar_no_overflow() {
        let area = Rect::new(0, 0, 10, 10);
        let (_, scrollbar_area) = split_area_for_scrollbar(area);
        let mut buf = Buffer::empty(area);

        // Content fits - should not render anything
        render_scrollbar(&mut buf, scrollbar_area, 5, 10, 0, false);

        // Check scrollbar column is empty (spaces with no custom background)
        let sb = scrollbar_area.unwrap();
        for y in 0..sb.height {
            let cell = &buf[(sb.x, sb.y + y)];
            assert_eq!(cell.symbol(), " ");
            // The cell should NOT have our scrollbar background colors
            // (i.e., it should be reset/default, not Color::Rgb)
            if let Some(Color::Rgb(_, _, _)) = cell.style().bg {
                panic!("Should not have RGB background when no scrollbar rendered");
            }
            // Otherwise - Reset, None, or other default-like value
        }
    }

    #[test]
    fn test_render_scrollbar_following_vs_not() {
        let area = Rect::new(0, 0, 10, 10);
        let (_, scrollbar_area) = split_area_for_scrollbar(area);

        // Render following
        let mut buf_following = Buffer::empty(area);
        render_scrollbar(&mut buf_following, scrollbar_area, 100, 10, 90, true);

        // Render not following
        let mut buf_not_following = Buffer::empty(area);
        render_scrollbar(&mut buf_not_following, scrollbar_area, 100, 10, 50, false);

        // The styles should differ - not following should be brighter
        let sb = scrollbar_area.unwrap();
        let following_style = buf_following[(sb.x, sb.y)].style();
        let not_following_style = buf_not_following[(sb.x, sb.y)].style();

        // Both should have backgrounds set (non-default)
        assert!(following_style.bg.is_some());
        assert!(not_following_style.bg.is_some());

        // At 256-color or truecolor, the backgrounds should be distinguishable.
        // At Basic (16-color) level, both dark grays map to Black — expected.
        if crate::theme::color_support::get().has_256() {
            assert_ne!(following_style.bg, not_following_style.bg);
        }
    }

    #[test]
    fn test_scrollbar_thumb_position() {
        let area = Rect::new(0, 0, 10, 10);
        let (_, scrollbar_area) = split_area_for_scrollbar(area);
        let sb = scrollbar_area.unwrap();

        // At top
        let mut buf_top = Buffer::empty(area);
        render_scrollbar(&mut buf_top, scrollbar_area, 100, 10, 0, false);

        // At bottom
        let mut buf_bottom = Buffer::empty(area);
        render_scrollbar(&mut buf_bottom, scrollbar_area, 100, 10, 90, false);

        // Count thumb cells (non-space)
        let count_thumb = |buf: &Buffer| -> usize {
            (0..sb.height)
                .filter(|&y| buf[(sb.x, sb.y + y)].symbol() != " ")
                .count()
        };

        // Both should have a thumb
        let top_thumb = count_thumb(&buf_top);
        let bottom_thumb = count_thumb(&buf_bottom);
        assert!(top_thumb > 0, "Should have thumb at top");
        assert!(bottom_thumb > 0, "Should have thumb at bottom");

        // Thumb size should be consistent
        assert_eq!(top_thumb, bottom_thumb, "Thumb size should be consistent");

        // Thumb position should differ (visual inspection would show top vs bottom)
        // We can check that the thumb cells are in different positions
        let thumb_positions = |buf: &Buffer| -> Vec<u16> {
            (0..sb.height)
                .filter(|&y| buf[(sb.x, sb.y + y)].symbol() != " ")
                .collect()
        };

        let top_pos = thumb_positions(&buf_top);
        let bottom_pos = thumb_positions(&buf_bottom);
        assert_ne!(
            top_pos, bottom_pos,
            "Thumb should be at different positions"
        );
    }
}
