// reason: visitor helper functions and types are part of the visitor API surface
// (visitor feature gate). They are pub(crate) and available for use by the converter
// and user code, but not all are exercised in every code path, producing dead_code
// warnings. The allow is intentional — these are public API helpers, not dead code.
#![allow(dead_code)]

//! Helper functions for visitor pattern integration.
//!
//! This module provides efficient utilities for building visitor contexts,
//! dispatching visitor callbacks, and handling visitor results during the
//! HTML→Markdown conversion process.
//!
//! # Design Goals
//!
//! - **Zero allocation when possible**: Reuse existing data structures
//! - **Minimal overhead**: Inline hot paths, avoid unnecessary clones
//! - **Type safety**: Leverage Rust's type system for correct visitor handling
//! - **Ergonomics**: Reduce boilerplate for common visitor patterns
//!
//! # Usage
//!
//! These helpers are designed to be used within the converter module during
//! the DOM traversal. They bridge the gap between the internal conversion
//! state and the public visitor API.

use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};

use crate::error::{ConversionError, Result};
use crate::visitor::{HtmlVisitor, NodeContext, NodeType, VisitResult};
use std::borrow::Cow;

/// Build a `NodeContext` from current parsing state.
///
/// Creates a complete `NodeContext` suitable for passing to visitor callbacks.
/// This function collects metadata about the current node from various sources:
/// - Tag name and attributes from the HTML element
/// - Depth and parent information from the DOM tree
/// - Index among siblings for positional awareness
/// - Inline/block classification
///
/// # Parameters
///
/// - `node_type`: Coarse-grained classification (Link, Image, Heading, etc.)
/// - `tag_name`: Raw HTML tag name (e.g., "div", "h1", "custom-element")
/// - `attributes`: All HTML attributes as key-value pairs
/// - `depth`: Nesting depth in the DOM tree (0 = root)
/// - `index_in_parent`: Zero-based index among siblings
/// - `parent_tag`: Parent element's tag name (None if root)
/// - `is_inline`: Whether this element is treated as inline vs block
///
/// # Returns
///
/// A fully populated `NodeContext` ready for visitor dispatch.
///
/// # Performance
///
/// This function performs minimal allocations:
/// - Clones `tag_name` (typically 2-10 bytes)
/// - Clones `parent_tag` if present (typically 2-10 bytes)
/// - Clones the attributes `BTreeMap` (heap allocation if non-empty)
///
/// For text nodes and simple elements without attributes, allocations are minimal.
///
/// # Examples
///
/// ```text
/// let ctx = build_node_context(
///     NodeType::Heading,
///     "h1",
///     &attrs,
///     1,
///     0,
///     Some("body"),
///     false,
/// );
/// ```
#[inline]
pub fn build_node_context<'a>(
    node_type: NodeType,
    tag_name: &'a str,
    attributes: &'a BTreeMap<String, String>,
    depth: usize,
    index_in_parent: usize,
    parent_tag: Option<&'a str>,
    is_inline: bool,
) -> NodeContext<'a> {
    NodeContext::with_borrowed_attributes(
        node_type,
        Cow::Borrowed(tag_name),
        attributes,
        depth,
        index_in_parent,
        parent_tag.map(Cow::Borrowed),
        is_inline,
    )
}

/// Dispatch a visitor callback and handle the result.
///
/// This is the core dispatcher for all visitor callbacks. It safely handles the
/// optional visitor, calls the callback function, and translates the `VisitResult`
/// into concrete control flow decisions.
///
/// # Type Parameters
///
/// - `F`: Visitor callback function type
///
/// # Parameters
///
/// - `visitor`: Optional visitor (wrapped in `Arc<Mutex<>>`)
/// - `callback`: Closure that invokes the appropriate visitor method
///
/// # Returns
///
/// - `Ok(Some(String))`: Custom markdown output from `VisitResult::Custom`
/// - `Ok(None)`: Continue with default behavior (`VisitResult::Continue`)
/// - `Err(Error)`: Stop conversion with error (`VisitResult::Error`)
///
/// The `VisitResult::Skip` and `VisitResult::PreserveHtml` variants are handled
/// by the caller based on context.
///
/// # Error Handling
///
/// - If the visitor panics during callback, the panic propagates normally
/// - If the visitor returns `VisitResult::Error`, this is converted to `Error::Visitor`
/// - `Mutex` lock failures panic on poison (should never happen with correct usage)
///
/// # Performance
///
/// - Zero-cost when visitor is None (common case)
/// - Single dynamic dispatch when visitor is present
/// - No allocations except for error messages
///
/// # Examples
///
/// ```text
/// let result = dispatch_visitor(
///     &visitor,
///     |v| v.visit_heading(&ctx, level, text, id),
/// )?;
///
/// match result {
///     Some(custom_output) => return Ok(custom_output),
///     None => { /* proceed with default conversion */ }
/// }
/// ```
#[inline]
/// # Errors
///
/// Returns an error if visitor dispatch fails.
pub fn dispatch_visitor<F>(visitor: &Option<Arc<Mutex<dyn HtmlVisitor + Send>>>, callback: F) -> Result<VisitorDispatch>
where
    F: FnOnce(&mut dyn HtmlVisitor) -> VisitResult,
{
    let Some(visitor_rc) = visitor else {
        return Ok(VisitorDispatch::Continue);
    };

    let result = {
        let mut visitor_ref = visitor_rc.lock().expect("visitor mutex poisoned");
        callback(&mut *visitor_ref)
    };

    match result {
        VisitResult::Continue => Ok(VisitorDispatch::Continue),
        VisitResult::Custom(output) => Ok(VisitorDispatch::Custom(output)),
        VisitResult::Skip => Ok(VisitorDispatch::Skip),
        VisitResult::PreserveHtml => Ok(VisitorDispatch::PreserveHtml),
        VisitResult::Error(msg) => Err(ConversionError::Visitor(msg)),
    }
}

/// Result of dispatching a visitor callback.
///
/// This enum represents the outcome of a visitor callback dispatch,
/// providing a more ergonomic interface for control flow than the
/// raw `VisitResult` type.
#[derive(Debug)]
pub enum VisitorDispatch {
    /// Continue with default conversion behavior
    Continue,

    /// Replace default output with custom markdown
    Custom(String),

    /// Skip this element entirely (don't output anything)
    Skip,

    /// Preserve original HTML (don't convert to markdown)
    PreserveHtml,
}

impl VisitorDispatch {
    /// Check if this dispatch result indicates continuation.
    #[inline]
    #[must_use]
    pub const fn is_continue(&self) -> bool {
        matches!(self, Self::Continue)
    }

    /// Check if this dispatch result contains custom output.
    #[inline]
    #[must_use]
    pub const fn is_custom(&self) -> bool {
        matches!(self, Self::Custom(_))
    }

    /// Check if this dispatch result indicates skipping.
    #[inline]
    #[must_use]
    pub const fn is_skip(&self) -> bool {
        matches!(self, Self::Skip)
    }

    /// Check if this dispatch result indicates HTML preservation.
    #[inline]
    #[must_use]
    pub const fn is_preserve_html(&self) -> bool {
        matches!(self, Self::PreserveHtml)
    }

    /// Extract custom output if present.
    #[inline]
    #[must_use]
    pub fn into_custom(self) -> Option<String> {
        match self {
            Self::Custom(output) => Some(output),
            _ => None,
        }
    }

    /// Extract custom output reference if present.
    #[inline]
    #[must_use]
    pub fn as_custom(&self) -> Option<&str> {
        match self {
            Self::Custom(output) => Some(output),
            _ => None,
        }
    }
}

/// Macro to reduce boilerplate when calling visitor methods.
///
/// This macro wraps the common pattern of:
/// 1. Check if visitor is present
/// 2. Call visitor method
/// 3. Handle early return for Custom/Skip/PreserveHtml/Error
/// 4. Continue with default behavior if visitor returns Continue
///
/// # Syntax
///
/// ```text
/// try_visitor!(visitor_option, method_name, ctx, arg1, arg2, ...);
/// ```
///
/// # Returns
///
/// - Returns early with custom output if visitor returns Custom/Skip/PreserveHtml
/// - Returns early with Err if visitor returns Error
/// - Continues execution if visitor returns Continue or is None
///
/// # Examples
///
/// ```text
/// // Before (verbose):
/// let dispatch = dispatch_visitor(&visitor, |v| v.visit_heading(&ctx, level, text, id))?;
/// match dispatch {
///     VisitorDispatch::Custom(output) => return Ok(output),
///     VisitorDispatch::Skip => return Ok(String::new()),
///     VisitorDispatch::PreserveHtml => return Ok(preserve_html_output),
///     VisitorDispatch::Continue => { /* proceed */ }
/// }
///
/// // After (concise):
/// try_visitor!(visitor, visit_heading, &ctx, level, text, id);
/// // Default conversion logic continues here...
/// ```
#[macro_export]
macro_rules! try_visitor {
    ($visitor:expr, $method:ident, $ctx:expr $(, $arg:expr)*) => {{
        let dispatch = $crate::visitor_helpers::dispatch_visitor(
            $visitor,
            |v| v.$method($ctx $(, $arg)*),
        )?;

        match dispatch {
            $crate::visitor_helpers::VisitorDispatch::Continue => {
            }
            $crate::visitor_helpers::VisitorDispatch::Custom(output) => {
                return Ok(output);
            }
            $crate::visitor_helpers::VisitorDispatch::Skip => {
                return Ok(String::new());
            }
            $crate::visitor_helpers::VisitorDispatch::PreserveHtml => {
                // Falls through to default conversion — full HTML preservation requires
                // the node handle and parser context which aren't available in this macro.
                // Callers that need PreserveHtml support should match on the dispatch
                // result directly and call serialize_tag_to_html.
            }
        }
    }};
}

/// Convenience macro for `element_start` visitor calls with early return.
///
/// This specialized macro handles the common pattern of calling `visit_element_start`
/// at the beginning of element processing. Unlike `try_visitor!`, this macro
/// understands that `element_start` callbacks typically want to abort processing
/// entirely if they return anything other than Continue.
///
/// # Syntax
///
/// ```text
/// try_visitor_element_start!(visitor_option, ctx);
/// ```
///
/// # Examples
///
/// ```text
/// fn process_heading(...) -> Result<String> {
///     let ctx = build_node_context(...);
///     try_visitor_element_start!(visitor, &ctx)?;
///
///     // Default heading processing continues here...
/// }
/// ```
#[macro_export]
macro_rules! try_visitor_element_start {
    ($visitor:expr, $ctx:expr) => {{
        $crate::try_visitor!($visitor, visit_element_start, $ctx);
    }};
}

/// Convenience macro for `element_end` visitor calls with output inspection.
///
/// This specialized macro handles the common pattern of calling `visit_element_end`
/// after generating default markdown output. The visitor receives the default
/// output and can choose to replace it or let it pass through.
///
/// # Syntax
///
/// ```text
/// try_visitor_element_end!(visitor_option, ctx, default_output_string);
/// ```
///
/// # Examples
///
/// ```text
/// fn process_heading(...) -> Result<String> {
///     let ctx = build_node_context(...);
///     let mut output = String::from("# Heading");
///
///     try_visitor_element_end!(visitor, &ctx, &output)?;
///     Ok(output)
/// }
/// ```
#[macro_export]
macro_rules! try_visitor_element_end {
    ($visitor:expr, $ctx:expr, $output:expr) => {{
        $crate::try_visitor!($visitor, visit_element_end, $ctx, $output);
    }};
}

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

    #[test]
    fn test_build_node_context() {
        let mut attrs = BTreeMap::new();
        attrs.insert("id".to_string(), "main".to_string());
        attrs.insert("class".to_string(), "container".to_string());

        let ctx = build_node_context(NodeType::Div, "div", &attrs, 2, 3, Some("body"), false);

        assert_eq!(ctx.node_type, NodeType::Div);
        assert_eq!(ctx.tag_name, "div");
        assert_eq!(ctx.depth, 2);
        assert_eq!(ctx.index_in_parent, 3);
        assert_eq!(ctx.parent_tag.as_deref(), Some("body"));
        assert!(!ctx.is_inline);
        assert_eq!(ctx.attributes().len(), 2);
        assert_eq!(ctx.attributes().get("id"), Some(&"main".to_string()));
    }

    #[test]
    fn test_build_node_context_no_parent() {
        let attrs = BTreeMap::new();

        let ctx = build_node_context(NodeType::Html, "html", &attrs, 0, 0, None, false);

        assert_eq!(ctx.node_type, NodeType::Html);
        assert_eq!(ctx.parent_tag, None);
        assert!(ctx.attributes().is_empty());
    }

    #[test]
    fn test_dispatch_visitor_none() {
        let visitor: Option<Arc<Mutex<dyn HtmlVisitor + Send>>> = None;

        let result = dispatch_visitor(&visitor, |v| {
            let ctx = NodeContext::with_borrowed_attributes(
                NodeType::Text,
                Cow::Borrowed(""),
                &EMPTY_ATTRS,
                0,
                0,
                None,
                true,
            );
            v.visit_text(&ctx, "test")
        })
        .unwrap();

        assert!(result.is_continue());
    }

    #[derive(Debug)]
    struct TestVisitor {
        mode: TestMode,
    }

    #[derive(Debug)]
    enum TestMode {
        Continue,
        Custom,
        Skip,
        PreserveHtml,
        Error,
    }

    impl HtmlVisitor for TestVisitor {
        fn visit_text(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {
            match self.mode {
                TestMode::Continue => VisitResult::Continue,
                TestMode::Custom => VisitResult::Custom(format!("CUSTOM: {text}")),
                TestMode::Skip => VisitResult::Skip,
                TestMode::PreserveHtml => VisitResult::PreserveHtml,
                TestMode::Error => VisitResult::Error("test error".to_string()),
            }
        }
    }

    #[test]
    fn test_dispatch_visitor_continue() {
        let visitor: Arc<Mutex<dyn HtmlVisitor + Send>> = Arc::new(Mutex::new(TestVisitor {
            mode: TestMode::Continue,
        }));
        let visitor_opt = Some(visitor);

        let ctx =
            NodeContext::with_borrowed_attributes(NodeType::Text, Cow::Borrowed(""), &EMPTY_ATTRS, 0, 0, None, true);

        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();

        assert!(result.is_continue());
    }

    #[test]
    fn test_dispatch_visitor_custom() {
        let visitor: Arc<Mutex<dyn HtmlVisitor + Send>> = Arc::new(Mutex::new(TestVisitor { mode: TestMode::Custom }));
        let visitor_opt = Some(visitor);

        let ctx =
            NodeContext::with_borrowed_attributes(NodeType::Text, Cow::Borrowed(""), &EMPTY_ATTRS, 0, 0, None, true);

        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();

        assert!(result.is_custom());
        assert_eq!(result.as_custom(), Some("CUSTOM: hello"));
    }

    #[test]
    fn test_dispatch_visitor_skip() {
        let visitor: Arc<Mutex<dyn HtmlVisitor + Send>> = Arc::new(Mutex::new(TestVisitor { mode: TestMode::Skip }));
        let visitor_opt = Some(visitor);

        let ctx =
            NodeContext::with_borrowed_attributes(NodeType::Text, Cow::Borrowed(""), &EMPTY_ATTRS, 0, 0, None, true);

        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();

        assert!(result.is_skip());
    }

    #[test]
    fn test_dispatch_visitor_preserve_html() {
        let visitor: Arc<Mutex<dyn HtmlVisitor + Send>> = Arc::new(Mutex::new(TestVisitor {
            mode: TestMode::PreserveHtml,
        }));
        let visitor_opt = Some(visitor);

        let ctx =
            NodeContext::with_borrowed_attributes(NodeType::Text, Cow::Borrowed(""), &EMPTY_ATTRS, 0, 0, None, true);

        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();

        assert!(result.is_preserve_html());
    }

    #[test]
    fn test_dispatch_visitor_error() {
        let visitor: Arc<Mutex<dyn HtmlVisitor + Send>> = Arc::new(Mutex::new(TestVisitor { mode: TestMode::Error }));
        let visitor_opt = Some(visitor);

        let ctx =
            NodeContext::with_borrowed_attributes(NodeType::Text, Cow::Borrowed(""), &EMPTY_ATTRS, 0, 0, None, true);

        let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello"));

        assert!(result.is_err());
        if let Err(ConversionError::Visitor(msg)) = result {
            assert_eq!(msg, "test error");
        } else {
            panic!("Expected Visitor error");
        }
    }

    #[test]
    fn test_visitor_dispatch_predicates() {
        let continue_dispatch = VisitorDispatch::Continue;
        assert!(continue_dispatch.is_continue());
        assert!(!continue_dispatch.is_custom());
        assert!(!continue_dispatch.is_skip());

        let custom_dispatch = VisitorDispatch::Custom("output".to_string());
        assert!(!custom_dispatch.is_continue());
        assert!(custom_dispatch.is_custom());
        assert!(!custom_dispatch.is_skip());

        let skip_dispatch = VisitorDispatch::Skip;
        assert!(!skip_dispatch.is_continue());
        assert!(!skip_dispatch.is_custom());
        assert!(skip_dispatch.is_skip());

        let preserve_dispatch = VisitorDispatch::PreserveHtml;
        assert!(!preserve_dispatch.is_continue());
        assert!(preserve_dispatch.is_preserve_html());
    }

    #[test]
    fn test_visitor_dispatch_into_custom() {
        let custom = VisitorDispatch::Custom("test".to_string());
        assert_eq!(custom.into_custom(), Some("test".to_string()));

        let continue_dispatch = VisitorDispatch::Continue;
        assert_eq!(continue_dispatch.into_custom(), None);
    }

    #[test]
    fn test_visitor_dispatch_as_custom() {
        let custom = VisitorDispatch::Custom("test".to_string());
        assert_eq!(custom.as_custom(), Some("test"));

        let skip = VisitorDispatch::Skip;
        assert_eq!(skip.as_custom(), None);
    }
}
