//! Utility traits for item lookup.

use crate::{Document, Item, PropertyValue};
use std::collections::HashSet;

/// Trait for filtering items by language.
pub trait LanguageFilter: Sized {
    /// Returns true if this item or any of its children match the given languages.
    fn matches_languages(&self, languages: &HashSet<&str>) -> bool;

    /// Returns a new instance with only items matching the given languages.
    fn filter_by_languages_set(&self, languages: &HashSet<&str>) -> Option<Self>;
}

/// Trait for finding items by property value.
pub trait FindItemByProperty {
    /// Finds all items with properties matching the predicate.
    fn find_items_with_matching_property_value_by<F>(&self, predicate: F) -> Vec<(String, Item)>
    where
        F: Fn(String, PropertyValue) -> bool + Copy;

    /// Finds all items with a property matching the given value.
    fn find_items_with_matching_property_value(
        &self,
        needle: PropertyValue,
    ) -> Vec<(String, Item)> {
        self.find_items_with_matching_property_value_by(|_name, property_value| {
            property_value == needle
        })
    }
}

impl FindItemByProperty for Item {
    fn find_items_with_matching_property_value_by<F>(&self, predicate: F) -> Vec<(String, Item)>
    where
        F: Fn(String, PropertyValue) -> bool + Copy,
    {
        let mut values = self
            .properties
            .iter()
            .filter_map(|(name, values)| {
                if values
                    .iter()
                    .any(|value| predicate(name.to_owned(), value.to_owned()))
                {
                    Some((name.to_owned(), self.to_owned()))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();

        self.children.iter().for_each(|child| {
            values.extend(child.find_items_with_matching_property_value_by(predicate));
        });

        values
    }
}

impl FindItemByProperty for Document {
    fn find_items_with_matching_property_value_by<F>(&self, predicate: F) -> Vec<(String, Item)>
    where
        F: Fn(String, PropertyValue) -> bool + std::marker::Copy,
    {
        self.items
            .iter()
            .flat_map(|item| item.find_items_with_matching_property_value_by(predicate))
            .collect()
    }
}

/// Trait for finding items by URL property.
pub trait FindItemByUrl: FindItemByProperty {
    /// Finds an item with a url property matching the expected URL.
    fn find_item_by_url(&self, expected_url: &url::Url) -> Option<Item> {
        let url_property_value = PropertyValue::Url(crate::UrlValue::new(expected_url.to_owned()));
        self.find_items_with_matching_property_value(url_property_value)
            .first()
            .map(|(_name, value)| value.to_owned())
    }
}

impl FindItemByUrl for Document {}

impl FindItemByUrl for Item {}

/// Trait for finding items by ID.
pub trait FindItemById {
    /// Finds an item with the given ID.
    fn find_item_by_id(&self, expected_id: &str) -> Option<Item>;
}

impl FindItemById for Item {
    fn find_item_by_id(&self, expected_id: &str) -> Option<Item> {
        if self.id == Some(expected_id.to_string()) {
            Some(self.to_owned())
        } else {
            self.children
                .iter()
                .find_map(|item| item.find_item_by_id(expected_id))
        }
    }
}

impl FindItemById for Document {
    fn find_item_by_id(&self, expected_id: &str) -> Option<Item> {
        self.items
            .iter()
            .find_map(|item| item.find_item_by_id(expected_id))
    }
}
