//! Parser module for Pandoc/Quarto documents.
//!
//! This module implements a single-pass parser that constructs a lossless syntax tree (CST) for
//! Quarto documents.

use crate::config::Config;
use crate::range_utils::find_incremental_restart_offset;
use crate::syntax::{SyntaxKind, SyntaxNode};
use rowan::{GreenNode, GreenToken, NodeOrToken};

pub mod blocks;
pub mod inlines;
pub mod utils;
pub mod yaml;

mod block_dispatcher;
mod core;

// Re-export main parser
pub use core::Parser;

/// Parses a Quarto document string into a syntax tree.
///
/// Single-pass architecture: blocks emit inline structure during parsing.
///
/// # Examples
///
/// ```rust
/// use panache::parser::parse;
///
/// let input = "# Heading\n\nParagraph text.";
/// let tree = parse(input, None);
/// println!("{:#?}", tree);
/// ```
///
/// # Arguments
///
/// * `input` - The Quarto document content to parse
/// * `config` - Optional configuration. If None, uses default config.
pub fn parse(input: &str, config: Option<Config>) -> SyntaxNode {
    let config = config.unwrap_or_default();
    Parser::new(input, &config).parse()
}

pub struct IncrementalParseResult {
    pub tree: SyntaxNode,
    pub reparse_range: (usize, usize),
    pub strategy: &'static str,
}

/// Incrementally update a syntax tree by reparsing either a bounded section
/// window (between top-level headings) or from a safe restart boundary to EOF.
pub fn parse_incremental_suffix(
    input: &str,
    config: Option<Config>,
    old_tree: &SyntaxNode,
    old_edit_range: (usize, usize),
    new_edit_range: (usize, usize),
) -> IncrementalParseResult {
    let config = config.unwrap_or_default();
    let input_len = input.len();

    let Some(old_edit) = normalize_range(old_edit_range) else {
        return full_reparse_result(input, &config);
    };
    let Some(new_edit) = normalize_range(new_edit_range) else {
        return full_reparse_result(input, &config);
    };
    if new_edit.1 > input_len {
        return full_reparse_result(input, &config);
    }

    if old_tree.kind() != SyntaxKind::DOCUMENT {
        return full_reparse_result(input, &config);
    }

    if let Some(section_window) =
        find_top_level_heading_section_window(old_tree, old_edit, new_edit, input_len)
        && let Some(result) = reparse_section_window(input, &config, old_tree, section_window)
    {
        return result;
    }

    let restart = find_incremental_restart_offset(old_tree, old_edit.0, old_edit.1);
    let old_restart = align_to_document_child_start(old_tree, restart);

    if (old_edit.0..old_edit.1).contains(&old_restart) {
        return full_reparse_result(input, &config);
    }

    let new_restart = map_old_offset_to_new(old_restart, old_edit, new_edit, input_len);
    if !input.is_char_boundary(new_restart) {
        return full_reparse_result(input, &config);
    }

    let suffix_text = &input[new_restart..];
    let suffix_tree = Parser::new(suffix_text, &config).parse();

    let mut children: Vec<NodeOrToken<GreenNode, GreenToken>> = old_tree
        .children_with_tokens()
        .filter_map(|element| {
            let range = element.text_range();
            let end: usize = range.end().into();
            if end <= old_restart {
                Some(element_to_green(element))
            } else {
                None
            }
        })
        .collect();
    children.extend(suffix_tree.children_with_tokens().map(element_to_green));

    let tree = SyntaxNode::new_root(GreenNode::new(SyntaxKind::DOCUMENT.into(), children));
    let len: usize = tree.text_range().end().into();

    IncrementalParseResult {
        tree,
        reparse_range: (new_restart, len),
        strategy: "suffix_window",
    }
}

fn normalize_range(range: (usize, usize)) -> Option<(usize, usize)> {
    (range.0 <= range.1).then_some(range)
}

fn full_reparse_result(input: &str, config: &Config) -> IncrementalParseResult {
    let tree = Parser::new(input, config).parse();
    let len: usize = tree.text_range().end().into();
    IncrementalParseResult {
        tree,
        reparse_range: (0, len),
        strategy: "full_reparse",
    }
}

fn align_to_document_child_start(tree: &SyntaxNode, offset: usize) -> usize {
    for child in tree.children_with_tokens() {
        let range = child.text_range();
        let start: usize = range.start().into();
        let end: usize = range.end().into();
        if offset <= start {
            return start;
        }
        if offset < end {
            return start;
        }
    }
    let len: usize = tree.text_range().end().into();
    len
}

fn map_old_offset_to_new(
    old_offset: usize,
    old_edit: (usize, usize),
    new_edit: (usize, usize),
    new_len: usize,
) -> usize {
    if old_offset <= old_edit.0 {
        return old_offset;
    }
    if old_offset >= old_edit.1 {
        let old_span = old_edit.1 - old_edit.0;
        let new_span = new_edit.1 - new_edit.0;
        let delta = new_span as isize - old_span as isize;
        return old_offset.saturating_add_signed(delta).min(new_len);
    }
    new_edit.1.min(new_len)
}

fn element_to_green(element: crate::syntax::SyntaxElement) -> NodeOrToken<GreenNode, GreenToken> {
    match element {
        NodeOrToken::Node(node) => NodeOrToken::Node(node.green().into_owned()),
        NodeOrToken::Token(token) => NodeOrToken::Token(token.green().to_owned()),
    }
}

#[derive(Debug, Clone, Copy)]
struct SectionWindow {
    old_start: usize,
    old_end: usize,
    new_start: usize,
    new_end: usize,
}

fn find_top_level_heading_section_window(
    old_tree: &SyntaxNode,
    old_edit: (usize, usize),
    new_edit: (usize, usize),
    new_len: usize,
) -> Option<SectionWindow> {
    let old_len: usize = old_tree.text_range().end().into();
    let mut previous_heading: Option<(usize, usize)> = None;
    let mut next_heading: Option<(usize, usize)> = None;

    for child in old_tree.children() {
        if child.kind() != SyntaxKind::HEADING {
            continue;
        }

        let range = child.text_range();
        let start: usize = range.start().into();
        let end: usize = range.end().into();

        if start <= old_edit.0 {
            previous_heading = Some((start, end));
        } else {
            next_heading = Some((start, end));
            break;
        }
    }

    let (previous_start, previous_end) = previous_heading?;
    let (next_start, next_end) = next_heading.unwrap_or((old_len, old_len));

    if ranges_intersect(old_edit, (previous_start, previous_end))
        || ranges_intersect(old_edit, (next_start, next_end))
    {
        return None;
    }

    // Be conservative and only use the section window for edits that are
    // strictly inside the section body (not touching heading boundaries).
    if old_edit.0 <= previous_end || old_edit.1 >= next_start {
        return None;
    }

    let new_start = map_old_offset_to_new(previous_start, old_edit, new_edit, new_len);
    let new_end = map_old_offset_to_new(next_start, old_edit, new_edit, new_len);
    if new_start >= new_end || new_end > new_len {
        return None;
    }

    Some(SectionWindow {
        old_start: previous_start,
        old_end: next_start,
        new_start,
        new_end,
    })
}

fn ranges_intersect(a: (usize, usize), b: (usize, usize)) -> bool {
    a.0 < b.1 && b.0 < a.1
}

fn reparse_section_window(
    input: &str,
    config: &Config,
    old_tree: &SyntaxNode,
    section_window: SectionWindow,
) -> Option<IncrementalParseResult> {
    if !input.is_char_boundary(section_window.new_start)
        || !input.is_char_boundary(section_window.new_end)
    {
        return None;
    }

    let reparsed_window = Parser::new(
        &input[section_window.new_start..section_window.new_end],
        config,
    )
    .parse();

    let mut children: Vec<NodeOrToken<GreenNode, GreenToken>> = Vec::new();
    let mut inserted_window = false;

    for element in old_tree.children_with_tokens() {
        let range = element.text_range();
        let start: usize = range.start().into();
        let end: usize = range.end().into();

        if end <= section_window.old_start {
            children.push(element_to_green(element));
            continue;
        }

        if start >= section_window.old_end {
            if !inserted_window {
                children.extend(reparsed_window.children_with_tokens().map(element_to_green));
                inserted_window = true;
            }
            children.push(element_to_green(element));
            continue;
        }

        // Overlapping element is replaced by the reparsed section window.
    }

    if !inserted_window {
        children.extend(reparsed_window.children_with_tokens().map(element_to_green));
    }

    let tree = SyntaxNode::new_root(GreenNode::new(SyntaxKind::DOCUMENT.into(), children));
    Some(IncrementalParseResult {
        tree,
        reparse_range: (section_window.new_start, section_window.new_end),
        strategy: "section_window",
    })
}

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

    fn apply_edit(text: &str, old: (usize, usize), insert: &str) -> String {
        let mut out = String::with_capacity(text.len() - (old.1 - old.0) + insert.len());
        out.push_str(&text[..old.0]);
        out.push_str(insert);
        out.push_str(&text[old.1..]);
        out
    }

    #[test]
    fn incremental_suffix_matches_full_parse_for_tail_edit() {
        let input = "# H\n\npara one\n\npara two\n\npara three\n";
        let old_tree = parse(input, None);
        let old_edit = (30, 35);
        let updated = apply_edit(input, old_edit, "tail section");
        let new_edit = (30, 42);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
        let full = parse(&updated, None);
        assert_eq!(inc.to_string(), full.to_string());
    }

    #[test]
    fn incremental_suffix_matches_full_parse_for_middle_edit() {
        let input = "# H\n\n- a\n- b\n\nfinal para\n";
        let old_tree = parse(input, None);
        let old_edit = (10, 11);
        let updated = apply_edit(input, old_edit, "alpha");
        let new_edit = (10, 15);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
        let full = parse(&updated, None);
        assert_eq!(inc.to_string(), full.to_string());
    }

    #[test]
    fn incremental_suffix_matches_full_parse_for_setext_transition() {
        let input = "Intro\nSecond\n\nTail\n";
        let old_tree = parse(input, None);
        let old_edit = (5, 5);
        let updated = apply_edit(input, old_edit, "\n-----");
        let new_edit = (5, 11);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
        let full = parse(&updated, None);
        assert_eq!(inc.to_string(), full.to_string());
    }

    #[test]
    fn incremental_suffix_matches_full_parse_for_lazy_blockquote_change() {
        let input = "> quoted\nlazy\n\nnext\n";
        let old_tree = parse(input, None);
        let old_edit = (9, 13);
        let updated = apply_edit(input, old_edit, "> line");
        let new_edit = (9, 15);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit).tree;
        let full = parse(&updated, None);
        assert_eq!(inc.to_string(), full.to_string());
    }

    #[test]
    fn incremental_uses_heading_section_window_when_available() {
        let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta section\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let start = input.find("beta").expect("beta in test input");
        let old_edit = (start, start + 4);
        let updated = apply_edit(input, old_edit, "BETA");
        let new_edit = (start, start + 4);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert!(
            inc.reparse_range.0 > 0,
            "section reparse should not start at 0"
        );
        assert!(
            inc.reparse_range.1 < updated.len(),
            "section reparse should stop before EOF"
        );
    }

    #[test]
    fn incremental_uses_section_window_for_last_section() {
        let input = "# Intro\n\nalpha\n\n# Last\n\nbeta section\n";
        let old_tree = parse(input, None);
        let start = input.find("beta").expect("beta in test input");
        let old_edit = (start, start + 4);
        let updated = apply_edit(input, old_edit, "BETA");
        let new_edit = (start, start + 4);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert!(
            inc.reparse_range.0 > 0,
            "last section should start at the last heading boundary"
        );
        assert_eq!(
            inc.reparse_range.1,
            updated.len(),
            "last section should end at EOF"
        );
    }

    #[test]
    fn incremental_does_not_use_section_window_when_edit_touches_heading() {
        let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let middle_start = input
            .find("# Middle")
            .expect("middle heading in test input");
        let old_edit = (middle_start, middle_start + 1);
        let updated = apply_edit(input, old_edit, "#");
        let new_edit = (middle_start, middle_start + 1);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert_eq!(
            inc.reparse_range.1,
            updated.len(),
            "edits on headings should avoid section-window reparsing"
        );
    }

    #[test]
    fn incremental_does_not_use_section_window_when_edit_crosses_next_heading() {
        let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let beta_start = input.find("beta").expect("beta in test input");
        let end_start = input.find("# End").expect("end heading in test input");
        let old_edit = (beta_start, end_start + 2);
        let updated = apply_edit(input, old_edit, "beta\n\n# ");
        let new_edit = (beta_start, beta_start + 8);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert_eq!(
            inc.reparse_range.1,
            updated.len(),
            "cross-heading edits should avoid section-window reparsing"
        );
    }

    #[test]
    fn incremental_ignores_nested_headings_for_window_boundaries() {
        let input = "# Intro\n\n> ## Nested\n> quote body\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let quote_start = input.find("quote body").expect("quote body in test input");
        let old_edit = (quote_start, quote_start + 5);
        let updated = apply_edit(input, old_edit, "QUOTE");
        let new_edit = (quote_start, quote_start + 5);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert!(
            inc.reparse_range.1 < updated.len(),
            "window boundary should be the next top-level heading, not nested heading"
        );
    }

    #[test]
    fn incremental_section_window_handles_list_tight_loose_transition() {
        let input = "# Intro\n\nprelude\n\n# Middle\n\n- one\n- two\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let two_start = input.find("- two").expect("list item in test input");
        let old_edit = (two_start, two_start);
        let updated = apply_edit(input, old_edit, "\n");
        let new_edit = (two_start, two_start + 1);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert!(
            inc.reparse_range.0 > 0 && inc.reparse_range.1 < updated.len(),
            "list transition inside section should remain section-bounded"
        );
    }

    #[test]
    fn incremental_section_window_handles_blockquote_lazy_transition() {
        let input = "# Intro\n\nprelude\n\n# Middle\n\n> quoted\nlazy line\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let lazy_start = input.find("lazy line").expect("lazy line in test input");
        let old_edit = (lazy_start, lazy_start);
        let updated = apply_edit(input, old_edit, "> ");
        let new_edit = (lazy_start, lazy_start + 2);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert!(
            inc.reparse_range.0 > 0 && inc.reparse_range.1 < updated.len(),
            "blockquote continuation change inside section should remain section-bounded"
        );
    }

    #[test]
    fn incremental_section_window_handles_fenced_div_with_nested_heading() {
        let input = "# Intro\n\nprelude\n\n# Middle\n\n::: {.callout-note}\n## Nested\nbody text\n:::\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let body_start = input.find("body text").expect("body text in test input");
        let old_edit = (body_start, body_start + 4);
        let updated = apply_edit(input, old_edit, "BODY");
        let new_edit = (body_start, body_start + 4);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert!(
            inc.reparse_range.0 > 0 && inc.reparse_range.1 < updated.len(),
            "fenced div edits should use top-level heading boundaries"
        );
    }

    #[test]
    fn incremental_handles_inserting_heading_inside_section_window() {
        let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let beta_start = input.find("beta").expect("beta in test input");
        let old_edit = (beta_start, beta_start);
        let updated = apply_edit(input, old_edit, "## Inserted\n\n");
        let new_edit = (beta_start, beta_start + "## Inserted\n\n".len());

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert_eq!(
            inc.strategy, "section_window",
            "heading insertions within a bounded section should remain section-window mode"
        );
    }

    #[test]
    fn incremental_falls_back_when_deleting_next_heading_boundary() {
        let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let end_start = input.find("# End\n").expect("end heading in test input");
        let old_edit = (end_start, end_start + "# End\n\n".len());
        let updated = apply_edit(input, old_edit, "");
        let new_edit = (end_start, end_start);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert_ne!(
            inc.strategy, "section_window",
            "heading deletions across boundaries should avoid section-window mode"
        );
    }

    #[test]
    fn incremental_falls_back_when_editing_blank_line_after_heading() {
        let input = "# Intro\n\nalpha\n\n# Middle\n\nbeta\n\n# End\n\nomega\n";
        let old_tree = parse(input, None);
        let boundary = input
            .find("# Middle\n\n")
            .expect("middle heading boundary in test input");
        let blank_line_start = boundary + "# Middle\n".len();
        let old_edit = (blank_line_start, blank_line_start + 1);
        let updated = apply_edit(input, old_edit, "");
        let new_edit = (blank_line_start, blank_line_start);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert_ne!(
            inc.strategy, "section_window",
            "heading-adjacent blank line edits should avoid section-window mode"
        );
    }

    #[test]
    fn incremental_handles_frontmatter_to_first_heading_edit() {
        let input = "---\ntitle: Demo\n---\n\n# Intro\n\nalpha\n\n# Next\n\nomega\n";
        let old_tree = parse(input, None);
        let title_start = input.find("Demo").expect("frontmatter value in test input");
        let old_edit = (title_start, title_start + 4);
        let updated = apply_edit(input, old_edit, "Updated Demo");
        let new_edit = (title_start, title_start + "Updated Demo".len());

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert_ne!(
            inc.strategy, "section_window",
            "frontmatter edits before first heading should use conservative mode"
        );
    }

    #[test]
    fn incremental_handles_frontmatter_delimiter_edit() {
        let input = "---\ntitle: Demo\n---\n\n# Intro\n\nalpha\n";
        let old_tree = parse(input, None);
        let first_delim_start = 0;
        let old_edit = (first_delim_start, first_delim_start + 3);
        let updated = apply_edit(input, old_edit, "----");
        let new_edit = (first_delim_start, first_delim_start + 4);

        let inc = parse_incremental_suffix(&updated, None, &old_tree, old_edit, new_edit);
        let full = parse(&updated, None);
        assert_eq!(inc.tree.to_string(), full.to_string());
        assert_ne!(
            inc.strategy, "section_window",
            "frontmatter delimiter edits should stay in conservative mode"
        );
    }
}
