//! Clipboard-style tree traversal helpers.
//!
//! The editor's primary tree is indent-based (see
//! [`Document::walk_blocks`]): every root block carries an `indent`
//! property that defines its visual depth. For clipboard data however
//! the React layer uses the explicit `block.children` field — tables
//! and toggles build a "classic" tree with parent→child pointers.
//!
//! These two representations coexist intentionally; these helpers
//! operate on the `children`-based tree so copy / paste round-trips
//! can rebuild table rows, cells, and toggle descendants faithfully.
//!
//! Ported from `packages/react/src/hooks/useBlockSelection.ts` —
//! `collectSubtree()` and `extractPlainText()`.
//!
//! [`Document::walk_blocks`]: crate::Document::walk_blocks

use std::collections::HashMap;

use crate::model::block::{Block, BlockId};

/// Walk `root_id` and all descendants reachable through `children` and
/// populate `out` in insertion order. Guards against cycles via the
/// `out.contains_key` check — once a block is visited it is never
/// revisited.
///
/// Mirrors the TS `collectSubtree` signature so the React wrapper can
/// delegate without shape conversion.
pub fn collect_subtree(
    root_id: &BlockId,
    all_blocks: &HashMap<BlockId, Block>,
    out: &mut HashMap<BlockId, Block>,
) {
    if out.contains_key(root_id) {
        return;
    }
    let Some(block) = all_blocks.get(root_id) else {
        return;
    };
    out.insert(root_id.clone(), block.clone());
    for child_id in &block.children {
        collect_subtree(child_id, all_blocks, out);
    }
}

/// Produce a plain-text rendering of `block` and all descendants.
///
/// Rules (matching the TS source byte-for-byte):
/// - Leaf blocks return their own inline text (`block.content`
///   concatenated).
/// - `table` blocks: rows joined by `\n`, cells joined by `\t`. Cell
///   text comes from `cell.content` directly — cells are treated as
///   leaves regardless of their own `children`. Intended for
///   spreadsheet-compatible clipboard paste.
/// - Any other block with children: own text, then each child's
///   recursive text, joined by `\n`. Empty child strings are filtered
///   out before the join to match the TS `filter(Boolean)` behaviour.
#[must_use]
pub fn extract_plain_text(block: &Block, by_id: &HashMap<BlockId, Block>) -> String {
    let own_text: String = block.content.iter().map(|s| s.text.as_str()).collect();
    if block.children.is_empty() {
        return own_text;
    }

    if block.ty == "table" {
        let mut row_texts: Vec<String> = Vec::with_capacity(block.children.len());
        for row_id in &block.children {
            let Some(row) = by_id.get(row_id) else {
                row_texts.push(String::new());
                continue;
            };
            let cell_texts: Vec<String> = row
                .children
                .iter()
                .map(|cell_id| match by_id.get(cell_id) {
                    Some(cell) => cell.content.iter().map(|s| s.text.as_str()).collect(),
                    None => String::new(),
                })
                .collect();
            row_texts.push(cell_texts.join("\t"));
        }
        return row_texts.join("\n");
    }

    let child_text: Vec<String> = block
        .children
        .iter()
        .filter_map(|cid| by_id.get(cid).map(|c| extract_plain_text(c, by_id)))
        .filter(|s| !s.is_empty())
        .collect();
    let joined = child_text.join("\n");
    if own_text.is_empty() {
        joined
    } else if joined.is_empty() {
        own_text
    } else {
        format!("{own_text}\n{joined}")
    }
}

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

    fn para(id: &str, text: &str) -> Block {
        let mut b = Block::new_paragraph(BlockId::new(id));
        b.content = vec![TextSpan::plain(text)];
        b
    }

    fn build_map(blocks: Vec<Block>) -> HashMap<BlockId, Block> {
        blocks.into_iter().map(|b| (b.id.clone(), b)).collect()
    }

    // ── collect_subtree ──

    #[test]
    fn collect_subtree_single_block() {
        let map = build_map(vec![para("a", "hi")]);
        let mut out = HashMap::new();
        collect_subtree(&BlockId::new("a"), &map, &mut out);
        assert_eq!(out.len(), 1);
        assert!(out.contains_key(&BlockId::new("a")));
    }

    #[test]
    fn collect_subtree_two_levels() {
        let mut parent = para("p", "top");
        parent.children = vec![BlockId::new("c1"), BlockId::new("c2")];
        let map = build_map(vec![parent, para("c1", "one"), para("c2", "two")]);
        let mut out = HashMap::new();
        collect_subtree(&BlockId::new("p"), &map, &mut out);
        assert_eq!(out.len(), 3);
    }

    #[test]
    fn collect_subtree_missing_children_tolerated() {
        let mut parent = para("p", "t");
        parent.children = vec![BlockId::new("ghost")];
        let map = build_map(vec![parent]);
        let mut out = HashMap::new();
        collect_subtree(&BlockId::new("p"), &map, &mut out);
        assert_eq!(out.len(), 1, "missing child must not panic");
    }

    #[test]
    fn collect_subtree_guards_against_cycles() {
        let mut a = para("a", "a");
        a.children = vec![BlockId::new("b")];
        let mut b = para("b", "b");
        b.children = vec![BlockId::new("a")]; // cycle!
        let map = build_map(vec![a, b]);
        let mut out = HashMap::new();
        collect_subtree(&BlockId::new("a"), &map, &mut out);
        assert_eq!(out.len(), 2, "cycle must terminate with both blocks seen");
    }

    #[test]
    fn collect_subtree_table_with_cells() {
        let mut t = Block::new(BlockId::new("t"), "table");
        t.children = vec![BlockId::new("r1")];
        let mut r = Block::new(BlockId::new("r1"), "table_row");
        r.children = vec![BlockId::new("c1"), BlockId::new("c2")];
        let mut c1 = Block::new(BlockId::new("c1"), "table_cell");
        c1.content = vec![TextSpan::plain("A")];
        let mut c2 = Block::new(BlockId::new("c2"), "table_cell");
        c2.content = vec![TextSpan::plain("B")];
        let map = build_map(vec![t, r, c1, c2]);
        let mut out = HashMap::new();
        collect_subtree(&BlockId::new("t"), &map, &mut out);
        assert_eq!(out.len(), 4);
    }

    // ── extract_plain_text ──

    #[test]
    fn plain_text_leaf() {
        let p = para("p", "hello");
        let map = build_map(vec![p.clone()]);
        assert_eq!(extract_plain_text(&p, &map), "hello");
    }

    #[test]
    fn plain_text_table_tabs_and_newlines() {
        let mut t = Block::new(BlockId::new("t"), "table");
        t.children = vec![BlockId::new("r1"), BlockId::new("r2")];
        let mut r1 = Block::new(BlockId::new("r1"), "table_row");
        r1.children = vec![BlockId::new("c1a"), BlockId::new("c1b")];
        let mut r2 = Block::new(BlockId::new("r2"), "table_row");
        r2.children = vec![BlockId::new("c2a"), BlockId::new("c2b")];
        let mut c1a = Block::new(BlockId::new("c1a"), "table_cell");
        c1a.content = vec![TextSpan::plain("A1")];
        let mut c1b = Block::new(BlockId::new("c1b"), "table_cell");
        c1b.content = vec![TextSpan::plain("B1")];
        let mut c2a = Block::new(BlockId::new("c2a"), "table_cell");
        c2a.content = vec![TextSpan::plain("A2")];
        let mut c2b = Block::new(BlockId::new("c2b"), "table_cell");
        c2b.content = vec![TextSpan::plain("B2")];

        let map = build_map(vec![t.clone(), r1, r2, c1a, c1b, c2a, c2b]);
        assert_eq!(extract_plain_text(&t, &map), "A1\tB1\nA2\tB2");
    }

    #[test]
    fn plain_text_generic_nested_blocks() {
        // Toggle + children: "title" then each child joined by newline.
        let mut toggle = Block::new(BlockId::new("t"), "toggle");
        toggle.content = vec![TextSpan::plain("title")];
        toggle.children = vec![BlockId::new("c1"), BlockId::new("c2")];
        let map = build_map(vec![
            toggle.clone(),
            para("c1", "child one"),
            para("c2", "child two"),
        ]);
        assert_eq!(
            extract_plain_text(&toggle, &map),
            "title\nchild one\nchild two"
        );
    }

    #[test]
    fn plain_text_drops_empty_child_descendants() {
        let mut parent = Block::new(BlockId::new("p"), "toggle");
        parent.content = vec![TextSpan::plain("top")];
        parent.children = vec![BlockId::new("c1"), BlockId::new("c2")];
        let empty = Block::new_paragraph(BlockId::new("c1")); // no content
        let real = para("c2", "two");
        let map = build_map(vec![parent.clone(), empty, real]);
        assert_eq!(extract_plain_text(&parent, &map), "top\ntwo");
    }

    #[test]
    fn plain_text_missing_child_tolerated() {
        let mut parent = Block::new_paragraph(BlockId::new("p"));
        parent.content = vec![TextSpan::plain("top")];
        parent.children = vec![BlockId::new("ghost")];
        let map = build_map(vec![parent.clone()]);
        assert_eq!(
            extract_plain_text(&parent, &map),
            "top",
            "missing child must not panic or emit stray newline"
        );
    }
}
