use std::sync::Arc;

use self::element::{LinkRelationExpander, MatchedElements};
use microformats_types::{Properties, PropertyValue};
use regex::Regex;
use swc_common::{BytePos, FileName, SourceFile};
use swc_html_codegen::Emit as _;
use swc_html_parser::parser::ParserConfig;

mod element;
mod property;
mod test;
mod value_class;

#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum Error {
    #[error("Failed to parse HTML: {0:?}")]
    Html(swc_html_parser::error::Error),

    #[error("Missing the parent item for a child item at the location {0:?}")]
    MissingParentItemForChild(element::Placement),

    #[error("Invalid property for expansion.")]
    InvalidPropertyExpansion,

    #[error("Could not determine which item to add a property to the location of {0:?}")]
    MissingParentItemForProperty(element::Placement),

    #[error(
        "Could not determine which parent item to define a property to from the location of {0:?}"
    )]
    MissingParentItemForPropertyDeclaration(element::Placement),

    #[error("A URL to base relative URLs in this document is required.")]
    UrlBaseForDocumentRequired,

    #[error(transparent)]
    Types(#[from] microformats_types::Error),

    #[error(transparent)]
    Fmt(#[from] std::fmt::Error),

    #[error(transparent)]
    Url(#[from] url::ParseError),
}

impl From<swc_html_parser::error::Error> for Error {
    fn from(value: swc_html_parser::error::Error) -> Self {
        Self::Html(value)
    }
}

impl From<microformats_types::temporal::Error> for Error {
    fn from(value: microformats_types::temporal::Error) -> Self {
        Self::Types(microformats_types::Error::from(value))
    }
}

lazy_static::lazy_static! {
    static ref RE_WHITESPACE: Regex = Regex::new(r"(\s)+").unwrap();
    static ref RE_CLASS_NAME: Regex = Regex::new(r#"^(?P<prefix>((h|p|u|dt|e){1}))-(?P<name>([a-z0-9]+-)?[a-z]+(-[a-z]+)*)$"#).unwrap();
}

#[allow(clippy::ptr_arg)]
fn non_empty_string(s: &String) -> bool {
    !s.is_empty()
}

fn non_empty_property_value(p: &PropertyValue) -> bool {
    !p.is_empty()
}

fn remove_surrounding_whitespace(text: impl ToString) -> String {
    text.to_string()
        .trim_matches(char::is_whitespace)
        .to_string()
}

fn merge_hash_maps(base_map: &mut Properties, addl_map: Properties) {
    for (property_name, property_value) in addl_map.into_iter() {
        if let Some(values) = base_map.get_mut(&property_name) {
            values.extend(property_value);
        } else {
            base_map.insert(property_name, property_value);
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElementRef {
    pub index: usize,
    pub node: element::Node,
}

pub type ElementPtr = Arc<ElementRef>;

#[derive(Clone)]
pub struct Parser {
    dom: swc_html_ast::Document,
}

impl std::fmt::Debug for Parser {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Parser")
    }
}

impl Parser {
    /// Parses the provided HTML into a DOM document prepared for Microformats parsing.
    ///
    /// # Errors
    ///
    /// This function will return an error if the HTML could not be parsed.
    #[tracing::instrument(level = "trace", err, fields(html = html.len()))]
    pub fn from_html(html: String) -> Result<Self, crate::Error> {
        let config = ParserConfig {
            scripting_enabled: false,
            iframe_srcdoc: false,
            allow_self_closing: true,
        };
        let mut html_errors = Default::default();
        let source_file = SourceFile::new(FileName::Anon, false, FileName::Anon, html, BytePos(1));
        let dom = swc_html_parser::parse_file_as_document(&source_file, config, &mut html_errors)
            .map_err(Error::from)?;

        drop(html_errors); // TODO: Report this back to the caller.

        Ok(Self { dom })
    }

    /// With the loaded DOM in memory, parses it into a [structured document][microformats_types::Document].
    ///
    /// # Errors
    ///
    /// This function will return an error if the DOM could not be parsed, relations could not be
    /// expanded or if items could not be expanded.
    #[tracing::instrument(level = "trace", skip(self), err, fields(base_url = base_url.as_ref().map(|u|u.to_string())))]
    pub fn into_document(
        self,
        base_url: Option<url::Url>,
    ) -> Result<microformats_types::Document, crate::Error> {
        let mut doc: microformats_types::Document = Default::default();
        let matched_elements = MatchedElements::for_document(&self.dom)?;

        let base_url = matched_elements
            .discern_base_url()
            .or(base_url)
            .ok_or(Error::UrlBaseForDocumentRequired)?;

        let link_relation_expander = LinkRelationExpander {
            base_url: base_url.clone(),
            elements: matched_elements.link_relation_elements(),
        };

        link_relation_expander.expand(&mut doc)?;

        for item_elem_ptr in matched_elements.top_level_elements() {
            let item = matched_elements.expand_item_from_element(item_elem_ptr, &base_url)?;
            doc.items.push(item)
        }

        Ok(doc)
    }
}
