//! Editor widget — purely a renderer, owns no state.
//!
//! Delegates the core rendering (per-byte style composition, clip/wrap,
//! gutter) to [`super::text_area`].

use std::collections::HashMap;

use ratatui::{
    buffer::Buffer as TuiBuffer,
    layout::Rect,
    style::Color,
    widgets::Widget,
};

use crate::editor::{
    buffer::Buffer,
    fold::FoldState,
    highlight::{StyledSpan, find_matching_brace},
};

use super::text_area::{
    self, RenderMode, TextContent,
    CURRENT_LINE_BG, FOLD_CLOSED_FG, FOLD_OPEN_FG, FOLD_PLACEHOLDER,
    GUTTER_CURRENT, GUTTER_NORMAL, MARKER_STYLE, TILDE_STYLE,
};

// Re-export so existing callers (`views/editor.rs`) can keep importing
// from `widgets::editor`.
pub use super::text_area::GutterMarker;
pub use super::text_area::gutter_width;

// ---------------------------------------------------------------------------
// BufferContent — TextContent adapter for Buffer + highlight cache
// ---------------------------------------------------------------------------

/// Implements [`TextContent`] for an editor buffer, pairing it with its
/// per-line syntax-highlight cache.
pub struct BufferContent<'a> {
    lines: Vec<String>,
    highlight_cache: &'a HashMap<usize, Vec<StyledSpan>>,
}

impl<'a> BufferContent<'a> {
    pub fn new(buffer: &'a Buffer, highlight_cache: &'a HashMap<usize, Vec<StyledSpan>>) -> Self {
        Self { lines: buffer.lines(), highlight_cache }
    }
}

impl TextContent for BufferContent<'_> {
    fn line_count(&self) -> usize {
        self.lines.len()
    }

    fn line_text(&self, idx: usize) -> &str {
        &self.lines[idx]
    }

    fn line_highlights(&self, idx: usize) -> Option<&[StyledSpan]> {
        self.highlight_cache.get(&idx).map(|v| v.as_slice())
    }
}

pub struct EditorWidget<'a> {
    pub buffer: &'a Buffer,
    pub folds: &'a FoldState,
    /// Per-line highlight cache keyed by line number.
    /// Lines not in this map render without syntax colours until computed.
    pub highlight_cache: &'a HashMap<usize, Vec<StyledSpan>>,
    pub gutter_markers: &'a [GutterMarker],
    /// 0-based line indices with added/modified git changes (lowest gutter priority).
    pub git_changed_lines: &'a std::collections::HashSet<usize>,
    /// All search matches as (row, byte_start, byte_end).
    pub search_matches: &'a [(usize, usize, usize)],
    /// Index into `search_matches` for the "current" (amber) match.
    pub search_current: Option<usize>,
    /// All selection spans as (row, byte_start, byte_end).
    pub selection_spans: &'a [(usize, usize, usize)],
    /// First visible screen column (horizontal scroll offset).
    pub scroll_x: usize,
    /// When true, long lines are soft-wrapped instead of clipped.
    pub word_wrap: bool,
    /// When true, line numbers are rendered in the gutter.
    pub show_line_numbers: bool,
    /// Tab width (number of spaces for tab character).
    pub tab_width: usize,
    pub gutter_bg: Color,
}

impl Widget for EditorWidget<'_> {
    fn render(self, area: Rect, buf: &mut TuiBuffer) {
        if area.height == 0 || area.width == 0 {
            return;
        }

        let content = BufferContent::new(self.buffer, self.highlight_cache);
        let lines = self.buffer.lines();
        let cursor = self.buffer.cursor();
        let scroll = self.buffer.scroll;
        let total_lines = content.line_count();
        let height = area.height as usize;

        let gutter_w = text_area::gutter_width(self.show_line_numbers, total_lines);
        let digit_w = if self.show_line_numbers {
            total_lines.to_string().len().max(3)
        } else {
            0
        };

        if area.width as usize <= gutter_w + 4 {
            return;
        }

        let content_x = area.x + gutter_w as u16;
        let content_w = area.width as usize - gutter_w;

        let brace_match = find_matching_brace(&lines, cursor.line, cursor.column);
        let visible = self.folds.visible_lines(&lines);

        let mut visual_row = 0usize;

        for (logical, line_text) in &visible {
            let logical = *logical;
            if logical < scroll {
                continue;
            }

            let screen_y = area.y + visual_row as u16;
            if screen_y >= area.y + area.height {
                break;
            }

            let is_current = logical == cursor.line;
            let row_bg = if is_current {
                CURRENT_LINE_BG
            } else {
                Color::Reset
            };

            // Gutter background (fill entire gutter column).
            let gutter_bg_color = if is_current { CURRENT_LINE_BG } else { self.gutter_bg };
            for gx in area.x..content_x {
                buf[(gx, screen_y)].set_bg(gutter_bg_color);
            }

            // Column positions for digits and a single marker/fold column followed by trailing separator.
            let (_fold_x, marker_x, _digits_x, _sep_x) = if self.show_line_numbers {
                let digits_x = area.x; // digits start at leftmost gutter
                let marker_x = digits_x + digit_w as u16; // single column for marker/fold to the right of digits
                let sep_x = marker_x + 1; // trailing space after marker column
                // Line number (right-aligned within digit_w)
                let num_str = format!("{:>width$}", logical + 1, width = digit_w);
                let num_style = if is_current {
                    GUTTER_CURRENT.bg(CURRENT_LINE_BG)
                } else {
                    GUTTER_NORMAL.bg(self.gutter_bg)
                };
                for (i, ch) in num_str.chars().enumerate() {
                    buf[(digits_x + i as u16, screen_y)]
                        .set_char(ch)
                        .set_style(num_style);
                }
                // draw trailing single space (gutter separator)
                buf[(sep_x, screen_y)].set_char(' ').set_style(
                    if is_current { GUTTER_CURRENT.bg(CURRENT_LINE_BG) } else { GUTTER_NORMAL.bg(self.gutter_bg) }
                );
                // fold and marker share the same column
                (marker_x, marker_x, digits_x, sep_x)
            } else {
                let marker_x = area.x; // single column for marker/fold
                let sep_x = area.x + 1; // trailing space
                let sp2_x = area.x + 2; // legacy extra spacer to preserve width when line numbers disabled
                buf[(sep_x, screen_y)].set_char(' ').set_style(
                    if is_current { GUTTER_CURRENT.bg(CURRENT_LINE_BG) } else { GUTTER_NORMAL.bg(self.gutter_bg) }
                );
                buf[(sp2_x, screen_y)].set_char(' ').set_bg(if is_current { CURRENT_LINE_BG } else { self.gutter_bg });
                (marker_x, marker_x, area.x, sep_x)
            };

            // Fold indicator (computed but drawn in the shared marker column).
            let (fold_ch, fold_fg) = if self.folds.is_folded_header(logical) {
                ('›', FOLD_CLOSED_FG)
            } else {
                let t = line_text.trim_end();
                let can_fold = t.ends_with('{')
                    || t.ends_with('(')
                    || t.ends_with('[')
                    || (logical + 1 < total_lines
                        && !line_text.trim().is_empty()
                        && text_area::indent_level(&lines[logical + 1])
                            > text_area::indent_level(line_text));
                if can_fold {
                    ('⌄', FOLD_OPEN_FG)
                } else {
                    (' ', Color::Reset)
                }
            };

            // Marker column shares the same cell as the fold indicator.
            //
            // Priority for BACKGROUND colour: issue marker > git change > normal gutter.
            // Priority for CHARACTER: issue marker symbol > fold indicator > blank.
            //
            // This means a git-changed line is always visible as a blue-tinted cell
            // (like a coloured left-margin bar), regardless of whether a fold indicator
            // is also present on that line.
            let issue_marker = self
                .gutter_markers
                .iter()
                .find(|m| m.line == logical)
                .map(|m| (m.symbol, m.style))
                .or_else(|| {
                    self.buffer
                        .markers
                        .iter()
                        .find(|m| m.line == logical)
                        .map(|m| (m.label.chars().next().unwrap_or('●'), MARKER_STYLE))
                });

            let git_changed = self.git_changed_lines.contains(&logical);

            // Resolve background: issue > git change > gutter.
            let cell_bg = if let Some((_, sty)) = issue_marker {
                sty.bg.unwrap_or(Color::LightRed)
            } else if git_changed {
                Color::Rgb(45, 125, 220) // blue — git change indicator
            } else {
                gutter_bg_color
            };

            // Resolve character + foreground.
            if let Some((sym, sty)) = issue_marker {
                // Issue marker: always wins on character unless a fold glyph overrides.
                let (ch, fg) = if fold_ch != ' ' {
                    (fold_ch, fold_fg)
                } else {
                    (sym, sty.fg.unwrap_or(Color::White))
                };
                buf[(marker_x, screen_y)].set_char(ch).set_fg(fg).set_bg(cell_bg);
            } else {
                // No issue marker: draw fold glyph if present, otherwise blank.
                // The cell_bg already reflects any git change.
                buf[(marker_x, screen_y)]
                    .set_char(fold_ch)
                    .set_fg(fold_fg)
                    .set_bg(cell_bg);
            }

            // trailing separator space was drawn earlier when computing sep_x

            // Fold placeholder.
            if self.folds.is_folded_header(logical) {
                let hidden = self
                    .folds
                    .folds()
                    .iter()
                    .find(|f| f.header == logical)
                    .map(|f| f.hidden_count())
                    .unwrap_or(0);
                let text = format!("{}  ··· {} lines folded", line_text, hidden);
                text_area::render_plain(
                    buf,
                    content_x,
                    screen_y,
                    content_w,
                    &text,
                    FOLD_PLACEHOLDER,
                    row_bg,
                );
                visual_row += 1;
                continue;
            }

            // Content.
            let spans: &[StyledSpan] = content.line_highlights(logical).unwrap_or(&[]);

            let mode = if self.word_wrap {
                RenderMode::Wrap { max_rows: (height - visual_row) as u16 }
            } else {
                RenderMode::Clip { scroll_x: self.scroll_x }
            };
            let rows_used = text_area::render_line_content(
                buf,
                content_x,
                screen_y,
                content_w,
                content.line_text(logical),
                spans,
                row_bg,
                logical,
                cursor.line,
                cursor.column,
                brace_match,
                self.search_matches,
                self.search_current,
                self.selection_spans,
                mode,
                self.tab_width,
            );
            // Blank gutter for continuation rows (word wrap only).
            for extra in 1..rows_used {
                let sy = area.y + (visual_row + extra) as u16;
                if sy < area.y + area.height {
                    let cont_bg = if is_current { CURRENT_LINE_BG } else { self.gutter_bg };
                    for gx in area.x..content_x {
                        buf[(gx, sy)].set_char(' ').set_bg(cont_bg);
                    }
                }
            }
            visual_row += rows_used;
        }

        // Tilde rows past EOF.
        for vy in visual_row..height {
            let screen_y = area.y + vy as u16;
            // Fill gutter background for empty rows past EOF
            for gx in area.x..content_x {
                buf[(gx, screen_y)].set_char(' ').set_bg(self.gutter_bg);
            }
            // Tilde at start of line within gutter
            buf[(area.x, screen_y)].set_char('~').set_style(TILDE_STYLE.bg(self.gutter_bg));
            // Clear content area
            for gx in content_x..(area.x + area.width) {
                buf[(gx, screen_y)].reset();
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::buffer::Buffer as TuiBuffer;
    use ratatui::layout::Rect;
    use ratatui::style::Style;
    use std::collections::{HashMap, HashSet};

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    fn make_widget<'a>(
        ebuf: &'a crate::editor::buffer::Buffer,
        folds: &'a crate::editor::fold::FoldState,
        highlight_cache: &'a HashMap<usize, Vec<crate::editor::highlight::StyledSpan>>,
        gutter_markers: &'a [GutterMarker],
        git_changed: &'a HashSet<usize>,
    ) -> EditorWidget<'a> {
        EditorWidget {
            buffer: ebuf,
            folds,
            highlight_cache,
            gutter_markers,
            git_changed_lines: git_changed,
            search_matches: &[],
            search_current: None,
            selection_spans: &[],
            scroll_x: 0,
            word_wrap: false,
            show_line_numbers: true,
            tab_width: 4,
            gutter_bg: Color::Rgb(28, 28, 36),
        }
    }

    /// Render the widget into a fresh buffer and return (tui_buf, marker_x, gutter_w).
    fn render(
        lines: Vec<&str>,
        gutter_markers: &[GutterMarker],
        git_changed: &HashSet<usize>,
    ) -> (TuiBuffer, u16, usize) {
        let buf_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
        let ebuf = crate::editor::buffer::Buffer::from_lines(buf_lines, None);
        let folds = crate::editor::fold::FoldState::default();
        let highlight_cache = HashMap::new();
        let widget = make_widget(&ebuf, &folds, &highlight_cache, gutter_markers, git_changed);

        let total_lines = ebuf.line_count();
        let gutter_w = crate::widgets::text_area::gutter_width(true, total_lines);
        let digit_w = total_lines.to_string().len().max(3);
        let marker_x = digit_w as u16; // marker col is right after the digits

        let area = Rect::new(0, 0, 80, lines.len() as u16 + 2);
        let mut tui_buf = TuiBuffer::empty(area);
        ratatui::widgets::Widget::render(widget, area, &mut tui_buf);

        (tui_buf, marker_x, gutter_w)
    }

    fn cell_bg(tui_buf: &TuiBuffer, x: u16, y: u16) -> Color {
        tui_buf[(x, y)].bg
    }

    // -----------------------------------------------------------------------
    // Tests — existing (no git changes)
    // -----------------------------------------------------------------------

    #[test]
    fn gutter_background_and_width_preserved() {
        let buf_lines = vec![
            "fn main() {".to_string(),
            "    println!(\"hello\");".to_string(),
            "}".to_string(),
        ];
        let ebuf = crate::editor::buffer::Buffer::from_lines(buf_lines, None);
        let folds = crate::editor::fold::FoldState::default();
        let highlight_cache = HashMap::new();
        let git_changed = HashSet::new();
        let widget = make_widget(&ebuf, &folds, &highlight_cache, &[], &git_changed);

        let area = Rect::new(0, 0, 80, 10);
        let mut tui_buf = TuiBuffer::empty(area);
        ratatui::widgets::Widget::render(widget, area, &mut tui_buf);

        let total_lines = ebuf.line_count();
        let gutter_w = crate::widgets::text_area::gutter_width(true, total_lines);
        let content_x = area.x + gutter_w as u16;

        // Ensure no vertical separator glyph (│) remains at the gutter/content boundary
        for row in 0..area.height {
            let y = area.y + row;
            let cell = &tui_buf[(content_x - 1, y)];
            let ch = cell.symbol().chars().next().unwrap_or(' ');
            assert_ne!(ch, '│', "Separator glyph still present at gutter boundary");
        }

        let first_line_y = area.y;
        let num_pos = content_x - 1;
        let cell = &tui_buf[(num_pos, first_line_y)];
        let ch = cell.symbol().chars().next().unwrap_or(' ');
        assert!(ch.is_ascii_digit() || ch == ' ', "Gutter rightmost cell should be a digit or space");
    }

    // -----------------------------------------------------------------------
    // Tests — git change indicator
    // -----------------------------------------------------------------------

    /// A git-changed line must have a blue background in the marker column.
    #[test]
    fn git_changed_line_has_blue_marker_bg() {
        let lines = vec!["unchanged", "changed", "also unchanged"];
        let mut git_changed = HashSet::new();
        git_changed.insert(1); // line 1 (0-based) is changed

        let (tui_buf, marker_x, _) = render(lines, &[], &git_changed);

        let unchanged_bg = cell_bg(&tui_buf, marker_x, 0);
        let changed_bg = cell_bg(&tui_buf, marker_x, 1);
        let also_unchanged_bg = cell_bg(&tui_buf, marker_x, 2);

        // Changed line must have the git-change blue background.
        assert_eq!(changed_bg, Color::Rgb(45, 125, 220), "git-changed line should have blue bg");

        // Unchanged lines must NOT have the git-change background.
        assert_ne!(unchanged_bg, Color::Rgb(45, 125, 220), "unchanged line should not have git bg");
        assert_ne!(also_unchanged_bg, Color::Rgb(45, 125, 220), "unchanged line should not have git bg");
    }

    /// Multiple git-changed lines all get blue bg.
    #[test]
    fn multiple_git_changed_lines() {
        let lines = vec!["a", "b", "c", "d", "e"];
        let mut git_changed = HashSet::new();
        git_changed.insert(0);
        git_changed.insert(2);
        git_changed.insert(4);

        let (tui_buf, marker_x, _) = render(lines, &[], &git_changed);

        assert_eq!(cell_bg(&tui_buf, marker_x, 0), Color::Rgb(45, 125, 220));
        assert_ne!(cell_bg(&tui_buf, marker_x, 1), Color::Rgb(45, 125, 220));
        assert_eq!(cell_bg(&tui_buf, marker_x, 2), Color::Rgb(45, 125, 220));
        assert_ne!(cell_bg(&tui_buf, marker_x, 3), Color::Rgb(45, 125, 220));
        assert_eq!(cell_bg(&tui_buf, marker_x, 4), Color::Rgb(45, 125, 220));
    }

    /// An empty git_changed_lines set produces no blue cells.
    #[test]
    fn no_git_changes_no_blue_bg() {
        let lines = vec!["line one", "line two"];
        let git_changed = HashSet::new();
        let (tui_buf, marker_x, _) = render(lines, &[], &git_changed);

        for row in 0..2u16 {
            assert_ne!(
                cell_bg(&tui_buf, marker_x, row),
                Color::Rgb(45, 125, 220),
                "No git changes → no blue bg on row {row}"
            );
        }
    }

    /// Issue markers take priority over git-change background.
    #[test]
    fn issue_marker_overrides_git_change_bg() {
        let lines = vec!["error line", "normal"];
        let mut git_changed = HashSet::new();
        git_changed.insert(0); // same line has both git change AND an issue marker

        let issue_bg = Color::Rgb(180, 30, 30);
        let markers = vec![GutterMarker {
            line: 0,
            symbol: '!',
            style: Style::new().fg(Color::White).bg(issue_bg),
        }];

        let (tui_buf, marker_x, _) = render(lines, &markers, &git_changed);

        // Issue marker bg wins over git bg.
        assert_eq!(
            cell_bg(&tui_buf, marker_x, 0),
            issue_bg,
            "Issue marker bg should override git-change bg"
        );
    }

    /// A git-changed line that also has a fold indicator (`⌄`) still gets the blue bg.
    #[test]
    fn git_changed_line_with_fold_indicator_has_blue_bg() {
        // "fn foo() {" ends with `{` → qualifies for fold indicator (⌄)
        let lines = vec!["fn foo() {", "    x", "}"];
        let mut git_changed = HashSet::new();
        git_changed.insert(0); // the line ending with `{` is git-changed

        let (tui_buf, marker_x, _) = render(lines, &[], &git_changed);

        assert_eq!(
            cell_bg(&tui_buf, marker_x, 0),
            Color::Rgb(45, 125, 220),
            "Git-changed line with fold indicator should still show blue bg"
        );
    }

    /// Issue marker on a fold-indicator line: issue bg wins (not git blue).
    #[test]
    fn issue_marker_on_foldable_git_changed_line() {
        let lines = vec!["fn bar() {", "    y", "}"];
        let mut git_changed = HashSet::new();
        git_changed.insert(0);

        let issue_bg = Color::Rgb(200, 80, 0);
        let markers = vec![GutterMarker {
            line: 0,
            symbol: 'W',
            style: Style::new().fg(Color::Black).bg(issue_bg),
        }];

        let (tui_buf, marker_x, _) = render(lines, &markers, &git_changed);

        assert_eq!(
            cell_bg(&tui_buf, marker_x, 0),
            issue_bg,
            "Issue marker bg should win over git bg even on foldable lines"
        );
    }
}
