//! Experimental extensions for microformats parsing.
//!
//! This module provides optional features that extend the core microformats functionality.
//! These features are opt-in via Cargo feature flags and allow users to enable
//! experimental capabilities while maintaining backward compatibility.
//!
//! # Available Features
//!
//! ## `cleaner` Feature
//! The `cleaner` feature provides the [`CleanedDocument`] trait for filtering
//! documents to contain only recognized microformat classes.
//!
//! # Example - Document Cleaning
//!
//! ```ignore
//! use microformats::extensions::CleanedDocument;
//!
//! let doc = microformats::from_html(html, &url)?;
//! let cleaned = doc.with_only_known_classes();
//! // Or using the shorter alias:
//! let cleaned = doc.as_cleaned();
//! ```
//!
//! ## `picture` Feature (Experimental)
//!
//! The `picture` feature enables enhanced parsing of HTML `<picture>` elements,
//! providing support for responsive images with multiple sources, srcset attributes,
//! and media queries.
//!
//! ### Enabling the Feature
//!
//! Add to your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! microformats = { version = "0.18", features = ["picture"] }
//! ```
//!
//! ### Picture Element Processing
//!
//! When enabled, the parser can handle complex picture elements:
//!
//! ```html
//! <picture class="u-photo">
//!     <source srcset="/images/hero-800w.jpg 800w, /images/hero-400w.jpg 400w"
//!             media="(min-width: 800px)" type="image/jpeg">
//!     <source srcset="/images/hero-600h.jpg 600h"
//!             media="(min-width: 600px)" type="image/webp">
//!     <img src="/images/fallback.jpg" alt="Hero image">
//! </picture>
//! ```
//!
//! ### Output Format
//!
//! Picture elements are parsed into structured data containing:
//! - Multiple `<source>` elements with their srcset, media, and type attributes
//! - Fallback `<img>` element information
//! - All URL resolution relative to the document base URL
//!
//! ### Supported Properties
//!
//! The picture feature works with standard microformats properties:
//! - `u-photo` - Photo images
//! - `u-logo` - Logo images
//! - `u-avatar` - Avatar images
//! - `p-name` - Name extraction from picture alt text
//!
//! ### Experimental Status
//!
//! ⚠️ **Note**: The picture parsing feature is experimental and subject to change.
//! The API and output format may evolve as the feature matures. Consider this
//! when building long-term dependencies on the picture parsing capabilities.
//!
//! This allows users to opt-in to experimental features while maintaining
//! backward compatibility for standard parsing.

use microformats_types::{Document, Item, Properties, PropertyValue};

/// Trait for cleaning documents of unrecognized microformat classes.
#[cfg(feature = "cleaner")]
pub trait CleanedDocument {
    /// Returns a new document containing only items with recognized microformat classes.
    ///
    /// This filters both top-level items and nested items (children and property values).
    fn with_only_known_classes(&self) -> Self;

    /// Returns a new document containing only items with recognized microformat classes.
    ///
    /// This is an alias for `with_only_known_classes()` and provides a more concise syntax.
    /// This filters both top-level items and nested items (children and property values).
    fn as_cleaned(&self) -> Self
    where
        Self: Sized,
    {
        self.with_only_known_classes()
    }

    /// Returns a list of items with unrecognized (custom) classes.
    fn unrecognized_items(&self) -> Vec<&Item>;
}

#[cfg(feature = "cleaner")]
impl CleanedDocument for Document {
    fn with_only_known_classes(&self) -> Self {
        let filtered_items = self.items.iter().filter_map(filter_item).collect();

        Self {
            items: filtered_items,
            url: self.url.clone(),
            rels: self.rels.clone(),
            lang: self.lang.clone(),
            #[cfg(feature = "metaformats")]
            meta_item: self.meta_item.as_ref().and_then(filter_item),
            #[cfg(feature = "debug_flow")]
            _debug_context: self._debug_context.clone(),
        }
    }

    fn unrecognized_items(&self) -> Vec<&Item> {
        let mut result = Vec::new();

        for item in &self.items {
            result.extend(collect_unrecognized_items(item));
        }

        result
    }
}

/// Returns `Some(item)` if all its classes are recognized, with nested items filtered.
/// Returns `None` if any class is unrecognized.
fn filter_item(item: &Item) -> Option<Item> {
    if item.r#type.iter().any(|c| !c.is_recognized()) {
        return None;
    }

    Some(Item {
        r#type: item.r#type.clone(),
        properties: filter_properties(&item.properties),
        children: item.children.iter().filter_map(filter_item).collect(),
        id: item.id.clone(),
        lang: item.lang.clone(),
        value: item.value.clone(),
    })
}

/// Filters property values, cleaning any nested items.
fn filter_properties(props: &Properties) -> Properties {
    props
        .iter()
        .map(|(key, values)| {
            let filtered_values = values
                .iter()
                .filter_map(|pv| match pv {
                    PropertyValue::Item(nested) => filter_item(nested).map(PropertyValue::Item),
                    _other => Some(pv.clone()),
                })
                .collect();
            (key.clone(), filtered_values)
        })
        .collect()
}

/// Collects all unrecognized items from an item tree.
fn collect_unrecognized_items<'a>(item: &'a Item) -> Vec<&'a Item> {
    let mut result = Vec::new();

    if item.r#type.iter().any(|c| !c.is_recognized()) {
        result.push(item);
    }

    // Check children
    for child in item.children.iter() {
        result.extend(collect_unrecognized_items(child));
    }

    // Check property values for nested items
    for values in item.properties.values() {
        for pv in values {
            if let PropertyValue::Item(nested) = pv {
                result.extend(collect_unrecognized_items(nested));
            }
        }
    }

    result
}

#[cfg(feature = "cleaner")]
mod test {
    use super::*;
    use microformats_types::{Class, Item, Items, KnownClass, PropertyValue};

    fn make_document_with_known_class() -> Document {
        Document {
            items: vec![Item {
                r#type: vec![Class::Known(KnownClass::Card)],
                properties: Default::default(),
                children: Items::from(vec![]),
                id: None,
                lang: None,
                value: None,
            }],
            url: None,
            rels: Default::default(),
            lang: None,
            #[cfg(feature = "metaformats")]
            meta_item: None,
            #[cfg(feature = "debug_flow")]
            _debug_context: Default::default(),
        }
    }

    fn make_document_with_custom_class() -> Document {
        Document {
            items: vec![Item {
                r#type: vec![Class::Custom("h-custom".to_string())],
                properties: Default::default(),
                children: Items::from(vec![]),
                id: None,
                lang: None,
                value: None,
            }],
            url: None,
            rels: Default::default(),
            lang: None,
            #[cfg(feature = "metaformats")]
            meta_item: None,
            #[cfg(feature = "debug_flow")]
            _debug_context: Default::default(),
        }
    }

    fn make_document_with_mixed_classes() -> Document {
        Document {
            items: vec![
                Item {
                    r#type: vec![Class::Known(KnownClass::Card)],
                    properties: Default::default(),
                    children: Items::from(vec![]),
                    id: None,
                    lang: None,
                    value: None,
                },
                Item {
                    r#type: vec![Class::Custom("h-custom".to_string())],
                    properties: Default::default(),
                    children: Items::from(vec![]),
                    id: None,
                    lang: None,
                    value: None,
                },
            ],
            url: None,
            rels: Default::default(),
            lang: None,
            #[cfg(feature = "metaformats")]
            meta_item: None,
            #[cfg(feature = "debug_flow")]
            _debug_context: Default::default(),
        }
    }

    fn make_document_with_nested_custom() -> Document {
        Document {
            items: vec![Item {
                r#type: vec![Class::Known(KnownClass::Entry)],
                properties: [(
                    "author".to_string(),
                    vec![PropertyValue::Item(Item {
                        r#type: vec![Class::Custom("h-custom".to_string())],
                        properties: Default::default(),
                        children: Items::from(vec![]),
                        id: None,
                        lang: None,
                        value: None,
                    })],
                )]
                .into_iter()
                .collect(),
                children: Items::from(vec![]),
                id: None,
                lang: None,
                value: None,
            }],
            url: None,
            rels: Default::default(),
            lang: None,
            #[cfg(feature = "metaformats")]
            meta_item: None,
            #[cfg(feature = "debug_flow")]
            _debug_context: Default::default(),
        }
    }

    fn make_document_with_children_custom() -> Document {
        Document {
            items: vec![Item {
                r#type: vec![Class::Known(KnownClass::Card)],
                properties: Default::default(),
                children: Items::from(vec![Item {
                    r#type: vec![Class::Custom("h-custom".to_string())],
                    properties: Default::default(),
                    children: Items::from(vec![]),
                    id: None,
                    lang: None,
                    value: None,
                }]),
                id: None,
                lang: None,
                value: None,
            }],
            url: None,
            rels: Default::default(),
            lang: None,
            #[cfg(feature = "metaformats")]
            meta_item: None,
            #[cfg(feature = "debug_flow")]
            _debug_context: Default::default(),
        }
    }

    #[test]
    fn with_only_known_classes_keeps_recognized_classes() {
        let doc = make_document_with_known_class();
        let cleaned = doc.with_only_known_classes();

        assert_eq!(cleaned.items.len(), 1);
        assert!(matches!(
            cleaned.items[0].r#type[0],
            Class::Known(KnownClass::Card)
        ));
    }

    #[test]
    fn with_only_known_classes_removes_custom_classes() {
        let doc = make_document_with_custom_class();
        let cleaned = doc.with_only_known_classes();

        assert!(cleaned.items.is_empty());
    }

    #[test]
    fn with_only_known_classes_filters_mixed() {
        let doc = make_document_with_mixed_classes();
        let cleaned = doc.with_only_known_classes();

        assert_eq!(cleaned.items.len(), 1);
        assert!(matches!(
            cleaned.items[0].r#type[0],
            Class::Known(KnownClass::Card)
        ));
    }

    #[test]
    fn with_only_known_classes_removes_nested_custom_in_properties() {
        let doc = make_document_with_nested_custom();
        let cleaned = doc.with_only_known_classes();

        assert_eq!(cleaned.items.len(), 1);
        let author_values = &cleaned.items[0].properties["author"];
        assert!(author_values.is_empty());
    }

    #[test]
    fn with_only_known_classes_removes_custom_children() {
        let doc = make_document_with_children_custom();
        let cleaned = doc.with_only_known_classes();

        assert_eq!(cleaned.items.len(), 1);
        assert!(cleaned.items[0].children.is_empty());
    }

    #[test]
    fn with_only_known_classes_preserves_url() {
        let url = url::Url::parse("https://example.com").unwrap();
        let mut doc = make_document_with_known_class();
        doc.url = Some(url.clone());

        let cleaned = doc.with_only_known_classes();

        assert_eq!(cleaned.url, Some(url));
    }

    #[test]
    fn with_only_known_classes_preserves_rels() {
        let url = url::Url::parse("https://example.com").unwrap();
        let mut doc = make_document_with_known_class();
        doc.rels.items.insert(
            url.clone(),
            microformats_types::Relation {
                rels: vec!["me".to_string()],
                ..Default::default()
            },
        );

        let cleaned = doc.with_only_known_classes();

        assert!(cleaned.rels.items.get(&url).is_some());
    }

    #[test]
    fn with_only_known_classes_preserves_lang() {
        let mut doc = make_document_with_known_class();
        doc.lang = Some("en".to_string());

        let cleaned = doc.with_only_known_classes();

        assert_eq!(cleaned.lang, Some("en".to_string()));
    }

    #[test]
    fn with_only_known_classes_empty_document() {
        let doc = Document {
            items: vec![],
            url: None,
            rels: Default::default(),
            lang: None,
            #[cfg(feature = "metaformats")]
            meta_item: None,
            #[cfg(feature = "debug_flow")]
            _debug_context: Default::default(),
        };

        let cleaned = doc.with_only_known_classes();

        assert!(cleaned.items.is_empty());
    }

    #[test]
    fn with_only_known_classes_all_custom() {
        let doc = make_document_with_custom_class();
        let cleaned = doc.with_only_known_classes();

        assert!(cleaned.items.is_empty());
    }

    #[test]
    fn as_cleaned_same_as_with_only_known_classes() {
        let doc = make_document_with_mixed_classes();
        let cleaned_with_method = doc.with_only_known_classes();
        let cleaned_with_alias = doc.as_cleaned();

        assert_eq!(
            cleaned_with_method.items.len(),
            cleaned_with_alias.items.len()
        );
        assert!(matches!(
            cleaned_with_alias.items[0].r#type[0],
            Class::Known(KnownClass::Card)
        ));
    }

    #[test]
    fn as_cleaned_with_known_classes() {
        let doc = make_document_with_known_class();
        let cleaned = doc.as_cleaned();

        assert_eq!(cleaned.items.len(), 1);
        assert!(matches!(
            cleaned.items[0].r#type[0],
            Class::Known(KnownClass::Card)
        ));
    }

    #[test]
    fn as_cleaned_removes_custom_classes() {
        let doc = make_document_with_custom_class();
        let cleaned = doc.as_cleaned();

        assert!(cleaned.items.is_empty());
    }

    #[test]
    fn unrecognized_items_returns_custom() {
        let doc = make_document_with_custom_class();
        let unrecognized = doc.unrecognized_items();

        assert_eq!(unrecognized.len(), 1);
    }

    #[test]
    fn unrecognized_items_empty_for_known() {
        let doc = make_document_with_known_class();
        let unrecognized = doc.unrecognized_items();

        assert!(unrecognized.is_empty());
    }

    #[test]
    fn unrecognized_items_finds_all_custom() {
        let doc = make_document_with_mixed_classes();
        let unrecognized = doc.unrecognized_items();

        assert_eq!(unrecognized.len(), 1);
    }

    #[test]
    fn unrecognized_items_finds_nested_custom() {
        let doc = make_document_with_nested_custom();
        let unrecognized = doc.unrecognized_items();

        assert_eq!(unrecognized.len(), 1);
    }

    #[test]
    fn unrecognized_items_finds_custom_children() {
        let doc = make_document_with_children_custom();
        let unrecognized = doc.unrecognized_items();

        assert_eq!(unrecognized.len(), 1);
    }

    #[test]
    fn unrecognized_items_empty_document() {
        let doc = Document {
            items: vec![],
            url: None,
            rels: Default::default(),
            lang: None,
            #[cfg(feature = "metaformats")]
            meta_item: None,
            #[cfg(feature = "debug_flow")]
            _debug_context: Default::default(),
        };

        let unrecognized = doc.unrecognized_items();

        assert!(unrecognized.is_empty());
    }
}
