//! Shared text area rendering — the common rendering engine for editor,
//! diff preview, and search preview panels.
//!
//! The [`TextAreaWidget`] handles:
//! - Per-byte style composition (syntax + overlays + cursor)
//! - Clip mode (horizontal scroll) and soft-wrap mode
//! - Optional gutter (line numbers, fold indicators, markers)
//! - Tab expansion
//!
//! Callers provide content through the [`TextContent`] trait and configure
//! rendering via [`TextAreaConfig`].

use ratatui::{
    buffer::Buffer as TuiBuffer,
    style::{Color, Modifier, Style},
};

use crate::editor::highlight::{StyledSpan, to_ratatui_style};
use unicode_width::UnicodeWidthChar;

// ---------------------------------------------------------------------------
// Style constants
// ---------------------------------------------------------------------------

pub(crate) const CURRENT_LINE_BG: Color = Color::Rgb(40, 40, 55);
pub(crate) const CURSOR_FG: Color = Color::Black;
pub(crate) const CURSOR_BG: Color = Color::White;
pub(crate) const BRACE_MATCH_FG: Color = Color::Yellow;
pub(crate) const BRACE_MATCH_BG: Color = Color::Rgb(80, 80, 0);
pub(crate) const GUTTER_NORMAL: Style = Style::new().fg(Color::DarkGray);
pub(crate) const GUTTER_CURRENT: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
pub(crate) const FOLD_OPEN_FG: Color = Color::Cyan;
pub(crate) const FOLD_CLOSED_FG: Color = Color::Yellow;
pub(crate) const FOLD_PLACEHOLDER: Style = Style::new()
    .fg(Color::DarkGray)
    .add_modifier(Modifier::ITALIC);
pub(crate) const MARKER_STYLE: Style = Style::new()
    .fg(Color::LightRed)
    .add_modifier(Modifier::BOLD);
pub(crate) const TILDE_STYLE: Style = Style::new().fg(Color::DarkGray);



const SEARCH_MATCH_BG: Color = Color::Rgb(100, 80, 0);
const SEARCH_CURRENT_BG: Color = Color::Rgb(230, 160, 0);
const SEARCH_CURRENT_FG: Color = Color::Black;
const SELECTION_BG: Color = Color::Rgb(50, 60, 100);

// ---------------------------------------------------------------------------
// TextContent trait
// ---------------------------------------------------------------------------

/// Read-only text content provider.
///
/// Abstracts over `Buffer` (editor), `Vec<DiffLine>` (diff), and
/// `Vec<PreviewLine>` (search preview).
pub trait TextContent {
    /// Total number of logical lines.
    fn line_count(&self) -> usize;

    /// Text of a logical line (no trailing newline).
    fn line_text(&self, idx: usize) -> &str;

    /// Syntax-highlight spans for a logical line, if available.
    fn line_highlights(&self, idx: usize) -> Option<&[StyledSpan]>;
}

// ---------------------------------------------------------------------------
// GutterMarker
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
pub struct GutterMarker {
    pub line: usize,
    pub symbol: char,
    pub style: Style,
}

// ---------------------------------------------------------------------------
// RenderMode
// ---------------------------------------------------------------------------

/// Controls whether a line is clipped (horizontal scroll) or soft-wrapped.
#[derive(Clone, Copy)]
pub enum RenderMode {
    /// Clip to the horizontal scroll window `[scroll_x, scroll_x + width)`.
    Clip { scroll_x: usize },
    /// Soft-wrap across at most `max_rows` screen rows.
    Wrap { max_rows: u16 },
}

// ---------------------------------------------------------------------------
// Gutter width calculation
// ---------------------------------------------------------------------------

/// Compute the gutter width in terminal columns.
///
/// New layout: [fold][marker][digits][space] — digits right-aligned, followed by a single space.
/// That is `digit_w + 3` columns when line numbers are shown.
/// Without line numbers: `[fold][marker][space]` — 3 columns.
pub fn gutter_width(show_line_numbers: bool, line_count: usize) -> usize {
    if show_line_numbers {
        let digit_w = line_count.to_string().len().max(3);
        // Layout: [digits][marker_or_fold][space] => digit_w + 2 columns
        digit_w + 2
    } else {
        // Preserve legacy width when line numbers are disabled (4 columns)
        4
    }
}

// ---------------------------------------------------------------------------
// Rendering core
// ---------------------------------------------------------------------------

/// Unified line renderer — handles both clip (non-wrap) and soft-wrap modes.
///
/// Builds the per-byte style array exactly once from spans, selection, search,
/// brace-match and cursor overlays, then dispatches to [`clip_render`] or
/// [`wrap_render`] depending on `mode`.
///
/// Returns the number of screen rows consumed (always 1 in clip mode).
#[allow(clippy::too_many_arguments)]
pub fn render_line_content(
    buf: &mut TuiBuffer,
    x: u16,
    start_y: u16,
    width: usize,
    line_text: &str,
    spans: &[StyledSpan],
    row_bg: Color,
    logical: usize,
    cursor_row: usize,
    cursor_col: usize,
    brace_match: Option<(usize, usize)>,
    search_matches: &[(usize, usize, usize)],
    search_current: Option<usize>,
    selection_spans: &[(usize, usize, usize)],
    mode: RenderMode,
    tab_width: usize,
) -> usize {
    let n = line_text.len();
    let mut styles = vec![Style::default().fg(Color::Reset).bg(row_bg); n + 1];

    // Syntax highlighting.
    for sp in spans {
        let start = sp.range.start.min(n);
        if start >= n {
            break;
        }
        let end = sp.range.end.min(n);
        if end <= start {
            continue;
        }
        let sty = to_ratatui_style(&sp.style).bg(row_bg);
        styles[start..end].fill(sty);
    }

    // Selection overlay — filter by logical row inline.
    for &(row, start, end) in selection_spans {
        if row != logical {
            continue;
        }
        let sty = Style::default().bg(SELECTION_BG);
        for s in styles.iter_mut().take(end.min(n)).skip(start) {
            *s = sty;
        }
    }

    // Search-match overlay — filter by logical row inline.
    for (idx, &(row, start, end)) in search_matches.iter().enumerate() {
        if row != logical {
            continue;
        }
        let is_current = Some(idx) == search_current;
        let (fg, bg) = if is_current {
            (SEARCH_CURRENT_FG, SEARCH_CURRENT_BG)
        } else {
            (Color::White, SEARCH_MATCH_BG)
        };
        let sty = Style::default().fg(fg).bg(bg);
        for s in styles.iter_mut().take(end.min(n)).skip(start) {
            *s = sty;
        }
    }

    let brace_style = Style::default()
        .fg(BRACE_MATCH_FG)
        .bg(BRACE_MATCH_BG)
        .add_modifier(Modifier::BOLD);
    let cursor_style = Style::default()
        .fg(CURSOR_FG)
        .bg(CURSOR_BG)
        .add_modifier(Modifier::BOLD);

    // Convert cursor column from character index to byte offset for brace matching
    let cursor_col_byte = line_text
        .char_indices()
        .nth(cursor_col)
        .map(|(i, _)| i)
        .unwrap_or(n);

    let cursor_is_brace = logical == cursor_row
        && cursor_col_byte < n
        && matches!(
            line_text.as_bytes()[cursor_col_byte],
            b'{' | b'}' | b'(' | b')' | b'[' | b']'
        );
    if cursor_is_brace {
        styles[cursor_col_byte] = brace_style;
    }
    if let Some((br, bc)) = brace_match
        && br == logical
        && bc < n
    {
        styles[bc] = brace_style;
    }

    match mode {
        RenderMode::Clip { scroll_x } => clip_render(
            buf, x, start_y, width, line_text, &styles, row_bg, n,
            logical, cursor_row, cursor_col, scroll_x, cursor_style, tab_width,
        ),
        RenderMode::Wrap { max_rows } => wrap_render(
            buf, x, start_y, width, line_text, &styles, row_bg, n,
            logical, cursor_row, cursor_col, max_rows, cursor_style, tab_width,
        ),
    }
}

/// Clip (non-wrap) rendering: single row, honoring `scroll_x`.
#[allow(clippy::too_many_arguments)]
pub fn clip_render(
    buf: &mut TuiBuffer,
    x: u16,
    y: u16,
    width: usize,
    line_text: &str,
    styles: &[Style],
    row_bg: Color,
    n: usize,
    logical: usize,
    cursor_row: usize,
    cursor_col: usize,
    scroll_x: usize,
    cursor_style: Style,
    tab_width: usize,
) -> usize {
    let mut screen_col = 0usize;
    let mut byte_idx = 0usize;
    let mut char_idx = 0usize;

    while screen_col < scroll_x + width {
        if byte_idx >= n {
            if logical == cursor_row && cursor_col >= char_idx && screen_col >= scroll_x {
                let vc = screen_col - scroll_x;
                buf[(x + vc as u16, y)]
                    .set_char(' ')
                    .set_style(cursor_style);
                screen_col += 1;
            }
            break;
        }
        let ch = line_text[byte_idx..].chars().next().unwrap_or(' ');
        if ch == '\n' || ch == '\r' {
            byte_idx += ch.len_utf8();
            continue;
        }
        if ch == '\t' {
            let spaces = tab_width - (screen_col % tab_width);
            let sty = if logical == cursor_row && char_idx == cursor_col {
                cursor_style
            } else {
                styles[byte_idx]
            };
            for s in 0..spaces {
                let sc = screen_col + s;
                if sc >= scroll_x && sc < scroll_x + width {
                    let vc = sc - scroll_x;
                    buf[(x + vc as u16, y)].set_char(' ').set_style(sty);
                }
            }
            screen_col += spaces;
            char_idx += 1;
        } else {
            let sty = if logical == cursor_row && char_idx == cursor_col {
                cursor_style
            } else {
                styles[byte_idx]
            };
            if screen_col >= scroll_x {
                let vc = screen_col - scroll_x;
                buf[(x + vc as u16, y)]
                    .set_char(ch)
                    .set_style(sty);
            }
            screen_col += 1;
            char_idx += 1;
        }
        byte_idx += ch.len_utf8();
    }

    let start_pad = screen_col.saturating_sub(scroll_x).min(width);
    for sc in start_pad..width {
        buf[(x + sc as u16, y)].set_char(' ').set_bg(row_bg);
    }
    1
}

/// Wrap rendering: may span multiple rows, no horizontal scroll.
#[allow(clippy::too_many_arguments)]
pub fn wrap_render(
    buf: &mut TuiBuffer,
    x: u16,
    start_y: u16,
    width: usize,
    line_text: &str,
    styles: &[Style],
    row_bg: Color,
    n: usize,
    logical: usize,
    cursor_row: usize,
    cursor_col: usize,
    max_rows: u16,
    cursor_style: Style,
    tab_width: usize,
) -> usize {
    let mut col_in_row = 0usize;
    let mut visual_row = 0usize;
    let mut byte_idx = 0usize;
    let mut char_idx = 0usize;

    while byte_idx < n {
        if visual_row >= max_rows as usize {
            break;
        }
        let ch = line_text[byte_idx..].chars().next().unwrap_or(' ');
        if ch == '\n' || ch == '\r' {
            byte_idx += ch.len_utf8();
            continue;
        }
        let char_w = if ch == '\t' { tab_width - (col_in_row % tab_width) } else { 1 };

        if col_in_row + char_w > width {
            for pc in col_in_row..width {
                buf[(x + pc as u16, start_y + visual_row as u16)]
                    .set_char(' ')
                    .set_bg(row_bg);
            }
            visual_row += 1;
            col_in_row = 0;
            if visual_row >= max_rows as usize {
                break;
            }
            let char_w2 = if ch == '\t' { 4 } else { 1 };
            if ch == '\t' {
                let sty = if logical == cursor_row && char_idx == cursor_col {
                    cursor_style
                } else {
                    styles[byte_idx]
                };
                for s in 0..char_w2 {
                    if s < width {
                        buf[(x + s as u16, start_y + visual_row as u16)]
                            .set_char(' ')
                            .set_style(sty);
                    }
                }
                col_in_row += char_w2;
            } else {
                let sty = if logical == cursor_row && char_idx == cursor_col {
                    cursor_style
                } else {
                    styles[byte_idx]
                };
                buf[(x + col_in_row as u16, start_y + visual_row as u16)]
                    .set_char(ch)
                    .set_style(sty);
                col_in_row += 1;
            }
        } else if ch == '\t' {
            let sty = if logical == cursor_row && char_idx == cursor_col {
                cursor_style
            } else {
                styles[byte_idx]
            };
            for s in 0..char_w {
                if col_in_row + s < width {
                    buf[(x + (col_in_row + s) as u16, start_y + visual_row as u16)]
                        .set_char(' ')
                        .set_style(sty);
                }
            }
            col_in_row += char_w;
        } else {
            let sty = if logical == cursor_row && char_idx == cursor_col {
                cursor_style
            } else {
                styles[byte_idx]
            };
            buf[(x + col_in_row as u16, start_y + visual_row as u16)]
                .set_char(ch)
                .set_style(sty);
            col_in_row += 1;
        }
        byte_idx += ch.len_utf8();
        char_idx += 1;
    }

    // End-of-line cursor.
    if logical == cursor_row
        && cursor_col >= char_idx
        && visual_row < max_rows as usize
        && col_in_row < width
    {
        buf[(x + col_in_row as u16, start_y + visual_row as u16)]
            .set_char(' ')
            .set_style(cursor_style);
        col_in_row += 1;
    }

    // Pad the last row.
    for pc in col_in_row..width {
        buf[(x + pc as u16, start_y + visual_row as u16)]
            .set_char(' ')
            .set_bg(row_bg);
    }

    visual_row + 1
}

/// Simple plain-text line renderer (used for fold placeholders, etc.).
pub fn render_plain(
    buf: &mut TuiBuffer,
    x: u16,
    y: u16,
    width: usize,
    text: &str,
    style: Style,
    bg: Color,
) {
    let s = style.bg(bg);
    for (ci, ch) in text.chars().enumerate() {
        if ci >= width {
            break;
        }
        buf[(x + ci as u16, y)].set_char(ch).set_style(s);
    }
    for ci in text.chars().count().min(width)..width {
        buf[(x + ci as u16, y)].set_char(' ').set_bg(bg);
    }
}

/// Count leading whitespace characters in a line.
pub fn indent_level(line: &str) -> usize {
    line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
}

/// Map a byte index within `line_text` to the visible screen column (taking
/// into account tab expansion and wide characters). `tab_width` controls how
/// many columns a '\t' advances to.
pub fn byte_to_screen_col(line_text: &str, byte_idx: usize, tab_width: usize) -> usize {
    let n = line_text.len();
    let byte_idx = byte_idx.min(n);
    let mut col: usize = 0;
    for (pos, ch) in line_text.char_indices() {
        if pos >= byte_idx { break; }
        if ch == '\t' {
            let rem = tab_width - (col % tab_width);
            col += rem;
        } else {
            col += UnicodeWidthChar::width(ch).unwrap_or(1);
        }
    }
    col
}