//! Inline AST node wrappers.

use super::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};

pub struct InlineMath(SyntaxNode);

impl AstNode for InlineMath {
    type Language = PanacheLanguage;

    fn can_cast(kind: SyntaxKind) -> bool {
        kind == SyntaxKind::INLINE_MATH
    }

    fn cast(syntax: SyntaxNode) -> Option<Self> {
        if Self::can_cast(syntax.kind()) {
            Some(Self(syntax))
        } else {
            None
        }
    }

    fn syntax(&self) -> &SyntaxNode {
        &self.0
    }
}

impl InlineMath {
    pub fn opening_marker(&self) -> Option<String> {
        self.0.children_with_tokens().find_map(|child| {
            child.into_token().and_then(|token| {
                (token.kind() == SyntaxKind::INLINE_MATH_MARKER).then(|| token.text().to_string())
            })
        })
    }

    pub fn closing_marker(&self) -> Option<String> {
        self.0
            .children_with_tokens()
            .filter_map(|child| child.into_token())
            .filter(|token| token.kind() == SyntaxKind::INLINE_MATH_MARKER)
            .nth(1)
            .map(|token| token.text().to_string())
    }

    /// The raw math content between the delimiters, reconstructed from the
    /// `MATH_CONTENT` subtree (excluding host container prefixes — see
    /// [`super::math::math_content_text`]).
    pub fn content(&self) -> String {
        super::math::math_content_text(&self.0)
    }

    pub fn content_range(&self) -> Option<rowan::TextRange> {
        let mut markers = self
            .0
            .children_with_tokens()
            .filter_map(|child| child.into_token())
            .filter(|token| token.kind() == SyntaxKind::INLINE_MATH_MARKER);

        let start = markers.next()?.text_range().end();
        let end = markers.next()?.text_range().start();
        (start <= end).then(|| rowan::TextRange::new(start, end))
    }
}

pub struct CodeSpan(SyntaxNode);

impl AstNode for CodeSpan {
    type Language = PanacheLanguage;

    fn can_cast(kind: SyntaxKind) -> bool {
        kind == SyntaxKind::INLINE_CODE
    }

    fn cast(syntax: SyntaxNode) -> Option<Self> {
        if Self::can_cast(syntax.kind()) {
            Some(Self(syntax))
        } else {
            None
        }
    }

    fn syntax(&self) -> &SyntaxNode {
        &self.0
    }
}

impl CodeSpan {
    pub fn marker(&self) -> Option<String> {
        self.0.children_with_tokens().find_map(|child| {
            child.into_token().and_then(|token| {
                (token.kind() == SyntaxKind::INLINE_CODE_MARKER).then(|| token.text().to_string())
            })
        })
    }

    pub fn content(&self) -> String {
        self.0
            .children_with_tokens()
            .filter_map(|child| child.into_token())
            .filter(|token| token.kind() == SyntaxKind::INLINE_CODE_CONTENT)
            .map(|token| token.text().to_string())
            .collect::<Vec<_>>()
            .join("")
    }

    pub fn content_range(&self) -> Option<rowan::TextRange> {
        let mut markers = self
            .0
            .children_with_tokens()
            .filter_map(|child| child.into_token())
            .filter(|token| token.kind() == SyntaxKind::INLINE_CODE_MARKER);

        let start = markers.next()?.text_range().end();
        let end = markers.next()?.text_range().start();
        (start <= end).then(|| rowan::TextRange::new(start, end))
    }
}

pub struct InlineHtml(SyntaxNode);

impl AstNode for InlineHtml {
    type Language = PanacheLanguage;

    fn can_cast(kind: SyntaxKind) -> bool {
        kind == SyntaxKind::INLINE_HTML
    }

    fn cast(syntax: SyntaxNode) -> Option<Self> {
        if Self::can_cast(syntax.kind()) {
            Some(Self(syntax))
        } else {
            None
        }
    }

    fn syntax(&self) -> &SyntaxNode {
        &self.0
    }
}

impl InlineHtml {
    pub fn verbatim(&self) -> String {
        self.0
            .children_with_tokens()
            .filter_map(|child| child.into_token())
            .filter(|token| token.kind() == SyntaxKind::INLINE_HTML_CONTENT)
            .map(|token| token.text().to_string())
            .collect()
    }

    pub fn is_comment(&self) -> bool {
        self.0
            .children_with_tokens()
            .filter_map(|child| child.into_token())
            .find(|token| token.kind() == SyntaxKind::INLINE_HTML_CONTENT)
            .is_some_and(|token| token.text().starts_with("<!--"))
    }
}

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

    #[test]
    fn inline_html_discriminates_comments_from_tags() {
        // Use <br/> rather than <span>: pandoc-dialect lifts <span>...</span>
        // to its own INLINE_HTML_SPAN kind, so it wouldn't surface here.
        let input = "Hi <!-- x --> <br/>\n";
        let tree = crate::parse(input, None);
        let spans: Vec<_> = tree.descendants().filter_map(InlineHtml::cast).collect();
        assert_eq!(spans.len(), 2, "expected 2 InlineHtml nodes");
        assert!(spans[0].is_comment(), "first span should be comment");
        assert_eq!(spans[0].verbatim(), "<!-- x -->");
        assert!(!spans[1].is_comment(), "second span should not be comment");
        assert_eq!(spans[1].verbatim(), "<br/>");
    }

    #[test]
    fn inline_math_extracts_markers_and_content() {
        let input = "Before $x^2 + y^2$ after\n";
        let tree = crate::parse(input, None);
        let math = tree
            .descendants()
            .find_map(InlineMath::cast)
            .expect("inline math");

        assert_eq!(math.opening_marker().as_deref(), Some("$"));
        assert_eq!(math.closing_marker().as_deref(), Some("$"));
        assert_eq!(math.content(), "x^2 + y^2");
        let range = math.content_range().expect("content range");
        let start: usize = range.start().into();
        let end: usize = range.end().into();
        assert_eq!(&input[start..end], "x^2 + y^2");
    }

    #[test]
    fn code_span_extracts_marker_and_content() {
        let input = "Use `code` here\n";
        let tree = crate::parse(input, None);
        let code = tree
            .descendants()
            .find_map(CodeSpan::cast)
            .expect("code span");

        assert_eq!(code.marker().as_deref(), Some("`"));
        assert_eq!(code.content(), "code");
        let range = code.content_range().expect("content range");
        let start: usize = range.start().into();
        let end: usize = range.end().into();
        assert_eq!(&input[start..end], "code");
    }
}
