//! Hover documentation floating box widget.
//!
//! Renders LSP hover content (plain text or Markdown) in a floating bordered
//! overlay.  Basic Markdown elements are rendered with terminal styling:
//! - Fenced code blocks (``` ... ```) — dark background, cyan text
//! - Inline code (`...`) — cyan text
//! - ATX headings (`#`, `##`, ...) — bold, heading marker stripped
//! - Bold spans (`**...**`) — BOLD modifier
//! - Italic spans (`*...*` / `_..._`) — ITALIC modifier
//! - Blank lines — preserved as empty separator lines
//! - Everything else — rendered as normal text

use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Widget},
};

const MAX_WIDTH: u16 = 70; // 68 content + 2 border
const MAX_LINES: u16 = 16; // 14 content + 2 border

/// Stateless hover info box widget.
///
/// Parses and renders `text` as Markdown inside a bordered floating box,
/// positioned above `(anchor_x, anchor_y)` when possible.
pub(crate) struct HoverWidget<'a> {
    pub text: &'a str,
    pub anchor_x: u16,
    pub anchor_y: u16,
    pub terminal_area: Rect,
}

impl Widget for HoverWidget<'_> {
    fn render(self, _area: Rect, buf: &mut Buffer) {
        let term_w = self.terminal_area.width;
        let term_h = self.terminal_area.height;
        let content_width = (MAX_WIDTH.saturating_sub(2)) as usize;

        let md_lines = render_markdown(self.text);
        let wrapped = wrap_styled_lines(md_lines, content_width);

        if wrapped.is_empty() {
            return;
        }

        let content_lines = wrapped.len().min((MAX_LINES.saturating_sub(2)) as usize);
        let box_w = wrapped
            .iter()
            .take(content_lines)
            .map(|l| line_display_width(l))
            .max()
            .unwrap_or(0) as u16
            + 2;
        let box_w = box_w.min(MAX_WIDTH).min(term_w);
        let box_h = content_lines as u16 + 2;

        let top = if self.anchor_y >= box_h {
            self.anchor_y - box_h
        } else {
            self.anchor_y + 1
        };
        let top = top.min(term_h.saturating_sub(box_h));
        let left = self.anchor_x.min(term_w.saturating_sub(box_w));

        let area = Rect { x: left, y: top, width: box_w, height: box_h };

        let block = Block::default()
            .borders(Borders::ALL)
            .style(Style::default().fg(Color::Cyan));
        let inner = block.inner(area);
        block.render(area, buf);

        for (i, line) in wrapped.iter().take(content_lines).enumerate() {
            let y = inner.y + i as u16;
            if y >= term_h {
                break;
            }
            let mut x = inner.x;
            for span in &line.spans {
                for ch in span.content.chars() {
                    if x >= inner.x + inner.width || x >= term_w {
                        break;
                    }
                    if let Some(cell) = buf.cell_mut((x, y)) {
                        cell.set_char(ch);
                        cell.set_style(span.style);
                    }
                    x += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
                }
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Markdown parser
// ---------------------------------------------------------------------------

/// Convert a Markdown string into a list of styled ratatui [`Line`]s.
pub(crate) fn render_markdown(text: &str) -> Vec<Line<'static>> {
    let normal        = Style::default().fg(Color::White);
    let code_fg       = Style::default().fg(Color::Cyan);
    let code_block_bg = Style::default().fg(Color::Cyan).bg(Color::Rgb(30, 30, 40));
    let heading_style = Style::default().fg(Color::White).add_modifier(Modifier::BOLD);

    let mut lines: Vec<Line<'static>> = Vec::new();
    let mut in_code_block = false;

    for raw in text.lines() {
        if raw.starts_with("```") {
            in_code_block = !in_code_block;
            continue;
        }

        if in_code_block {
            lines.push(Line::from(Span::styled(raw.to_owned(), code_block_bg)));
            continue;
        }

        if raw.starts_with('#') {
            let stripped = raw.trim_start_matches('#').trim();
            if stripped.is_empty() {
                lines.push(Line::default());
            } else {
                lines.push(Line::from(Span::styled(stripped.to_owned(), heading_style)));
            }
            continue;
        }

        let trimmed = raw.trim();
        if trimmed.len() >= 3
            && (trimmed.chars().all(|c| c == '-')
                || trimmed.chars().all(|c| c == '*')
                || trimmed.chars().all(|c| c == '_'))
        {
            lines.push(Line::default());
            continue;
        }

        if trimmed.is_empty() {
            lines.push(Line::default());
            continue;
        }

        lines.push(parse_inline(raw, normal, code_fg));
    }

    while lines.first().map(|l| l.spans.is_empty()).unwrap_or(false) {
        lines.remove(0);
    }
    while lines.last().map(|l| l.spans.is_empty()).unwrap_or(false) {
        lines.pop();
    }

    lines
}

/// Parse inline Markdown elements in a single line.
fn parse_inline(line: &str, normal: Style, code_fg: Style) -> Line<'static> {
    let bold_style   = normal.add_modifier(Modifier::BOLD);
    let italic_style = normal.add_modifier(Modifier::ITALIC);

    let chars: Vec<char> = line.chars().collect();
    let n = chars.len();
    let mut spans: Vec<Span<'static>> = Vec::new();
    let mut i = 0usize;
    let mut buf = String::new();

    macro_rules! flush {
        () => {
            if !buf.is_empty() {
                spans.push(Span::styled(std::mem::take(&mut buf), normal));
            }
        };
    }

    while i < n {
        // Inline code: `...`
        if chars[i] == '`' {
            flush!();
            let start = i + 1;
            if let Some(p) = chars[start..].iter().position(|&c| c == '`') {
                let code: String = chars[start..start + p].iter().collect();
                spans.push(Span::styled(code, code_fg));
                i = start + p + 1;
                continue;
            }
        }

        // Bold: **...**
        if i + 1 < n && chars[i] == '*' && chars[i + 1] == '*' {
            flush!();
            let start = i + 2;
            if let Some(p) = chars[start..].windows(2).position(|w| w == ['*', '*']) {
                let bold_text: String = chars[start..start + p].iter().collect();
                spans.push(Span::styled(bold_text, bold_style));
                i = start + p + 2;
                continue;
            }
        }

        // Italic: *...* or _..._ (not preceded by same char to avoid double-star)
        if (chars[i] == '*' || chars[i] == '_') && (i == 0 || chars[i - 1] != chars[i]) {
            let delim = chars[i];
            flush!();
            let start = i + 1;
            if let Some(p) = chars[start..].iter().position(|&c| c == delim) {
                let ital_text: String = chars[start..start + p].iter().collect();
                spans.push(Span::styled(ital_text, italic_style));
                i = start + p + 1;
                continue;
            }
        }

        buf.push(chars[i]);
        i += 1;
    }
    flush!();

    if spans.is_empty() {
        Line::from(Span::styled(line.to_owned(), normal))
    } else {
        Line::from(spans)
    }
}

// ---------------------------------------------------------------------------
// Word-wrap styled lines
// ---------------------------------------------------------------------------

fn wrap_styled_lines(lines: Vec<Line<'static>>, max_width: usize) -> Vec<Line<'static>> {
    let mut out = Vec::new();
    for line in lines {
        if line.spans.is_empty() {
            out.push(Line::default());
            continue;
        }
        let chars: Vec<(char, Style)> = line.spans
            .iter()
            .flat_map(|s| s.content.chars().map(move |c| (c, s.style)))
            .collect();

        let mut current: Vec<(char, Style)> = Vec::new();
        let mut current_width = 0usize;
        let mut word_buf: Vec<(char, Style)> = Vec::new();
        let mut word_width = 0usize;

        for (ch, sty) in &chars {
            if *ch == ' ' {
                if !word_buf.is_empty() {
                    let needed = if current_width == 0 { word_width } else { 1 + word_width };
                    if current_width + needed > max_width && !current.is_empty() {
                        out.push(chars_to_line(&current));
                        current.clear();
                        current_width = 0;
                    }
                    if current_width > 0 {
                        current.push((' ', Style::default()));
                        current_width += 1;
                    }
                    current.extend_from_slice(&word_buf);
                    current_width += word_width;
                    word_buf.clear();
                    word_width = 0;
                }
            } else {
                word_buf.push((*ch, *sty));
                word_width += unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1);
            }
        }
        if !word_buf.is_empty() {
            let needed = if current_width == 0 { word_width } else { 1 + word_width };
            if current_width + needed > max_width && !current.is_empty() {
                out.push(chars_to_line(&current));
                current.clear();
                current_width = 0;
            }
            if current_width > 0 {
                current.push((' ', Style::default()));
            }
            current.extend_from_slice(&word_buf);
        }
        if !current.is_empty() {
            out.push(chars_to_line(&current));
        }
    }
    out
}

fn chars_to_line(chars: &[(char, Style)]) -> Line<'static> {
    if chars.is_empty() {
        return Line::default();
    }
    let mut spans: Vec<Span<'static>> = Vec::new();
    let mut buf = String::new();
    let mut cur_style = chars[0].1;
    for &(ch, sty) in chars {
        if sty != cur_style {
            if !buf.is_empty() {
                spans.push(Span::styled(std::mem::take(&mut buf), cur_style));
            }
            cur_style = sty;
        }
        buf.push(ch);
    }
    if !buf.is_empty() {
        spans.push(Span::styled(buf, cur_style));
    }
    Line::from(spans)
}

fn line_display_width(line: &Line<'_>) -> usize {
    line.spans
        .iter()
        .map(|s| unicode_width::UnicodeWidthStr::width(s.content.as_ref()))
        .sum()
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn rendered_texts(md: &str) -> Vec<String> {
        render_markdown(md)
            .iter()
            .map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect::<String>())
            .collect()
    }

    #[test]
    fn plain_text_passthrough() {
        assert_eq!(rendered_texts("hello world"), ["hello world"]);
    }

    #[test]
    fn blank_lines_preserved() {
        let t = rendered_texts("first\n\nsecond");
        assert_eq!(t, ["first", "", "second"]);
    }

    #[test]
    fn fenced_code_block_styled() {
        let md = "```rust\nfn foo() {}\n```";
        let lines = render_markdown(md);
        assert_eq!(lines.len(), 1, "fence delimiters stripped");
        let text: String = lines[0].spans.iter().map(|s| s.content.as_ref()).collect();
        assert_eq!(text, "fn foo() {}");
        assert_eq!(lines[0].spans[0].style.fg, Some(Color::Cyan));
    }

    #[test]
    fn fenced_code_block_followed_by_blank() {
        let md = "```\ncode\n```\n\nafter";
        let lines = render_markdown(md);
        assert_eq!(lines.len(), 3);
        assert!(lines[1].spans.is_empty());
    }

    #[test]
    fn heading_stripped_and_bold() {
        let lines = render_markdown("## My Section");
        assert_eq!(lines.len(), 1);
        let text: String = lines[0].spans.iter().map(|s| s.content.as_ref()).collect();
        assert_eq!(text, "My Section");
        assert!(lines[0].spans[0].style.add_modifier.contains(Modifier::BOLD));
    }

    #[test]
    fn inline_code_styled() {
        let lines = render_markdown("use `foo()` here");
        let code_span = lines[0].spans.iter().find(|s| s.content == "foo()").unwrap();
        assert_eq!(code_span.style.fg, Some(Color::Cyan));
    }

    #[test]
    fn bold_text_styled() {
        let lines = render_markdown("some **bold** text");
        let bold_span = lines[0].spans.iter().find(|s| s.content == "bold").unwrap();
        assert!(bold_span.style.add_modifier.contains(Modifier::BOLD));
    }

    #[test]
    fn italic_text_styled() {
        let lines = render_markdown("some *italic* text");
        let ital_span = lines[0].spans.iter().find(|s| s.content == "italic").unwrap();
        assert!(ital_span.style.add_modifier.contains(Modifier::ITALIC));
    }

    #[test]
    fn horizontal_rule_becomes_blank() {
        let lines = render_markdown("before\n---\nafter");
        assert_eq!(lines.len(), 3);
        assert!(lines[1].spans.is_empty(), "hr becomes blank line");
    }

    #[test]
    fn empty_input_produces_no_lines() {
        assert!(render_markdown("").is_empty());
        assert!(render_markdown("   \n\n  ").is_empty());
    }

    #[test]
    fn realistic_rust_hover() {
        let md = "```rust\npub fn greet(name: &str) -> String\n```\n\nGreets the given **name**.";
        let lines = render_markdown(md);
        assert!(lines.len() >= 3, "code + blank + paragraph");
        let code_text: String = lines[0].spans.iter().map(|s| s.content.as_ref()).collect();
        assert!(code_text.contains("greet"));
        assert!(lines[1].spans.is_empty(), "blank separator after code block");
    }
}
