//! detail 头部的简介（album / artist / playlist）多行渲染与滚动视口。
//!
//! 数据层简介按 `\n` 保留原始换行与空行（作者有意的段落间隔）；这里把它拆成逻辑行、再按
//! 显示宽度词感知折行成可视行，给一个 `Cell` 维护的滚动 offset 开窗口。拉丁词整体不拆，
//! 无空格的长 CJK 串按列逐字符断。

use std::cell::Cell;

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Widget};

use crate::components::layout::shared::scrollbar::draw_scrollbar;
use crate::components::layout::shared::text::{char_width, display_width};
use crate::render::theme::Theme;

/// 把简介原文折成可视行序列：先按 `\n` 拆逻辑行（空 / 纯空白行原样保留，不压不并），
/// 每条逻辑行再按显示宽度 `width` 词感知折行。`text` 空或 `width==0` → 空序列。
///
/// # Params:
///   - `text`: 简介原文（含 `\n`）
///   - `width`: 可视行宽（列）
///
/// # Return:
///   折行后的可视行（每条 ≤ `width` 列）。
pub(crate) fn wrap_description(text: &str, width: u16) -> Vec<String> {
    if text.is_empty() || width == 0 {
        return Vec::new();
    }
    let sanitized = sanitize_controls(text);
    let mut rows = Vec::<String>::new();
    for logical in sanitized.split('\n') {
        wrap_logical(logical, width, &mut rows);
    }
    rows
}

/// 把除 `\n`（折行靠它分逻辑行）外的控制字符压成空格。
///
/// 数据层简介里巡演日程等用 `\t` 当列分隔：制表符显示宽 0，若原样进 cell symbol，终端会
/// 按自身 tab stop 推进光标、错乱整行后续字符（实测网易云专辑简介渲染乱码）。这里在折行前
/// 统一抹平——`\t` → 空格后由词感知折行当词间空白吃掉（列对齐在窄面板里本就保不住），`\r`
/// 等其余控制字符同样压成空格，杜绝任何控制符落进可视 cell。
fn sanitize_controls(text: &str) -> String {
    text.chars()
        .map(|c| if c == '\n' || !c.is_control() { c } else { ' ' })
        .collect()
}

/// 折一条逻辑行进 `rows`：纯空白 / 空行原样保真为一条（作者有意的段落间隔，含「单空格」
/// spacer）；否则按空格切词、词感知贪心折行——拉丁词整体不拆，超过整行宽的词（无空格的长
/// CJK 串 / 超长英文）才逐字符硬断。
fn wrap_logical(line: &str, width: u16, rows: &mut Vec<String>) {
    if line.chars().all(char::is_whitespace) {
        rows.push(line.to_owned());
        return;
    }
    let mut row = String::new();
    let mut row_w = 0u16;
    for word in line.split(' ') {
        // 连续 / 首尾空格产生的空 word：跳过（行内多空格归一为一个，由词间补空格还原）。
        if word.is_empty() {
            continue;
        }
        let ww = display_width(word);
        if ww > width {
            // 词本身就宽过整行：先冲掉当前行，再逐字符硬断（CJK 串走这条 = 逐字符断）。
            if !row.is_empty() {
                rows.push(std::mem::take(&mut row));
                row_w = 0;
            }
            hard_break(word, width, rows, &mut row, &mut row_w);
            continue;
        }
        // 普通词：放不下就换行（换行后行首不补空格）；放得下且行非空则补一个词间空格。
        let need = if row.is_empty() {
            ww
        } else {
            ww.saturating_add(1)
        };
        if row_w.saturating_add(need) > width {
            rows.push(std::mem::take(&mut row));
            row.push_str(word);
            row_w = ww;
        } else {
            if !row.is_empty() {
                row.push(' ');
                row_w = row_w.saturating_add(1);
            }
            row.push_str(word);
            row_w = row_w.saturating_add(ww);
        }
    }
    if !row.is_empty() {
        rows.push(row);
    }
}

/// 把超长词逐字符硬断进 `rows`，尾部不满一行的残段留在 `row`/`row_w`（供后续词接续）。
fn hard_break(word: &str, width: u16, rows: &mut Vec<String>, row: &mut String, row_w: &mut u16) {
    for ch in word.chars() {
        let w = char_width(ch);
        // 单字符就宽过整行（极端：双宽字符撞上 width==1）：自成一行，防死循环。
        if w > width {
            if !row.is_empty() {
                rows.push(std::mem::take(row));
                *row_w = 0;
            }
            rows.push(ch.to_string());
            continue;
        }
        if row_w.saturating_add(w) > width {
            rows.push(std::mem::take(row));
            *row_w = 0;
        }
        row.push(ch);
        *row_w = row_w.saturating_add(w);
    }
}

/// 把滚动 `offset` 钳进 `[0, max(0, total - viewport)]`（`total`/`viewport` 为可视行数）。
pub(crate) fn clamp_scroll(offset: u16, total: u16, viewport: u16) -> u16 {
    offset.min(total.saturating_sub(viewport))
}

/// 画简介滚动视口：折行 → 按 `scroll` 开窗 → 末列溢出时画滚动条。`scroll` 在此被钳进内容
/// 边界并**写回**（render 走 `&self`，故内部可变；消除过滚后反向的死区）。`text` 空 / 区域
/// 退化时不画。
///
/// # Params:
///   - `area`: 简介区（含末列滚动条位）
///   - `text`: 简介原文
///   - `scroll`: 滚动 offset（可视行）；钳后写回
pub(crate) fn draw_description(
    buf: &mut Buffer,
    area: Rect,
    text: &str,
    scroll: &Cell<u16>,
    theme: &Theme,
) {
    if area.height == 0 || area.width == 0 || text.is_empty() {
        return;
    }
    // 末列留给滚动条（恒定预留，版面不随是否溢出抖动）。
    let text_w = area.width.saturating_sub(1).max(1);
    let rows = wrap_description(text, text_w);
    let total = u16::try_from(rows.len()).unwrap_or(u16::MAX);
    let viewport = area.height;
    let off = clamp_scroll(scroll.get(), total, viewport);
    scroll.set(off);
    let dim = Style::new().fg(theme.overlay);
    let visible = rows
        .iter()
        .skip(usize::from(off))
        .take(usize::from(viewport))
        .map(|r| Line::from(Span::styled(r.clone(), dim)))
        .collect::<Vec<Line<'static>>>();
    let text_area = Rect::new(area.x, area.y, text_w, area.height);
    Widget::render(Paragraph::new(visible), text_area, buf);
    if total > viewport {
        draw_scrollbar(buf, area, total, viewport, off, theme);
    }
}

#[cfg(test)]
mod tests {
    use super::{clamp_scroll, wrap_description};

    /// 换行保真：`\n` 拆成独立可视行（修「整段塞一个 Line、换行被吞」的 bug）。
    #[test]
    fn wrap_splits_on_newline() {
        assert_eq!(wrap_description("alpha\nbeta", 40), vec!["alpha", "beta"]);
    }

    /// 空行 / 纯空白行原样保留、不压不并（作者有意的段落间隔）。
    #[test]
    fn wrap_preserves_blank_lines() {
        // 连续空行不塌成一行。
        assert_eq!(
            wrap_description("a\n\n\nb", 40),
            vec!["a", "", "", "b"],
            "三个 \\n 给出两条空行,不合并"
        );
        // 网易云那种「只含一个空格」的 spacer 行保真。
        assert_eq!(wrap_description("a\n \nb", 40), vec!["a", " ", "b"]);
    }

    /// CJK 双宽断行：width=4 时每行恰两个中文字（每字 2 列）。
    #[test]
    fn wrap_cjk_by_display_width() {
        assert_eq!(
            wrap_description("胜利或失败", /*width*/ 4),
            vec!["胜利", "或失", "败"]
        );
    }

    /// 词感知折行：拉丁词整体不拆，放不下就整体换行（不再「Fo / otball」拦腰断）。
    #[test]
    fn wrap_keeps_latin_words_whole() {
        assert_eq!(
            wrap_description("Chinese Football", /*width*/ 8),
            vec!["Chinese", "Football"]
        );
    }

    /// 超长词（宽过整行）逐字符硬断，残段接续。
    #[test]
    fn wrap_hard_breaks_overlong_word() {
        assert_eq!(
            wrap_description("Footballer", /*width*/ 8),
            vec!["Football", "er"]
        );
    }

    /// 巡演日程等用制表符当列分隔：`\t` 必须压成词间空白，绝不能原样进可视行。
    /// （原样落 cell 时终端按自身 tab stop 推进光标 → 整行后续字符错乱，实测专辑简介乱码。）
    #[test]
    fn wrap_normalizes_tabs_to_space() {
        assert_eq!(
            wrap_description("3月5日\t周二\t贵阳", /*width*/ 40),
            vec!["3月5日 周二 贵阳"]
        );
    }

    /// 纯制表符的逻辑行也压成空格 spacer，不把 `\t` 漏进输出。
    #[test]
    fn wrap_normalizes_tab_only_line() {
        assert_eq!(wrap_description("x\n\t\ny", 40), vec!["x", " ", "y"]);
    }

    /// 空原文 / 零宽 → 空序列（不渲染）。
    #[test]
    fn wrap_empty_yields_nothing() {
        assert!(wrap_description("", 40).is_empty());
        assert!(wrap_description("anything", 0).is_empty());
    }

    /// 滚动钳制：内容短于视口恒 0；溢出时上界 = total-viewport。
    #[test]
    fn clamp_scroll_bounds() {
        assert_eq!(clamp_scroll(/*offset*/ 5, /*total*/ 3, /*viewport*/ 10), 0);
        assert_eq!(clamp_scroll(100, 30, 10), 20, "上界 total-viewport");
        assert_eq!(clamp_scroll(7, 30, 10), 7, "界内不动");
    }
}
